All articles
Publishing a plugin
Publishing from GitHub Actions
Publish a GitHub release and let a workflow sign and upload the plugin.
You can publish new versions from GitHub Actions. You publish a GitHub release, and a workflow signs the plugin and uploads it to the marketplace. There is no password or API key to store: GitHub gives every workflow run a token that says which repository it comes from, and the marketplace checks it. Every release still goes through the checks and review.
- The action: prusa3d/plugin-marketplace-publish
- An example plugin that publishes this way: prusa3d/plugin-marketplace-example. It is a template: Use this template on GitHub gives you a repository of your own with the workflows already in place, and its README walks through the setup below.
Set it up
-
A signing key. Create a key pair and register the public key under Signing keys, as in Signing keys. Skip this if you already have one.
-
A namespace. The plugin id must be in one of your namespaces: a verified domain, or
io.github.<your account>.*, which the first release from a repository of that account proves by itself. -
A trusted publisher. Under Trusted publishing in the author console, add the plugin id, the repository (
owner/name), the workflow file name (release.yml) and, optionally, a GitHub environment. -
The secret. Store
private_key.pemas the secretPRUSA_SLICER_PLUGIN_MARKETPLACE_SIGNING_KEY, best in a GitHub environment that only release tags may use (see Security):gh secret set PRUSA_SLICER_PLUGIN_MARKETPLACE_SIGNING_KEY --env release < private_key.pemWhoever has the private key can sign plugins as you. Never commit it, keep a backup in a safe place such as a password manager (neither the marketplace nor GitHub can give it back), and if it leaks, revoke it and register a new one.
-
The workflow. Add this file to the repository:
name: Publish to Plugin Marketplace on: release: types: [published] permissions: contents: read id-token: write # lets the run prove which repository and workflow it is jobs: publish: runs-on: ubuntu-latest environment: release # optional, see Security steps: - uses: actions/checkout@v7 - uses: prusa3d/plugin-marketplace-publish@v1 with: path: . # the folder with manifest.json private-key: ${{ secrets.PRUSA_SLICER_PLUGIN_MARKETPLACE_SIGNING_KEY }} -
A release. Bump
versioninmanifest.json, commit, and publish a GitHub release taggedv<version>. A pre-release goes to the beta channel, and the release notes become the changelog.gh release create v1.2.0 --title "1.2.0" --notes "What changed"
The release then shows on your plugin’s page in the author console.
Several plugins in one repository
Keep each plugin in its own folder and add one trusted publisher per plugin, all with the same
repository and workflow. Each release publishes one plugin: its tag names the folder and the
version, for example flow-tower/v1.2.0.
your-account/slicer-plugins
├── .github/workflows/release.yml
└── plugins/
├── flow-tower/
│ ├── manifest.json "id": "io.github.your-account.flow-tower"
│ └── main.lua
└── gridfinity/
├── manifest.json "id": "io.github.your-account.gridfinity"
├── main.lua
└── assets/bin.svg
The workflow picks the folder from the tag:
steps:
- uses: actions/checkout@v7
- name: Pick the plugin from the tag
id: plugin
run: |
name="${GITHUB_REF_NAME%/v*}" # flow-tower/v1.2.0 -> flow-tower
dir="plugins/$name"
if [ ! -f "$dir/manifest.json" ]; then
echo "::error::tag $GITHUB_REF_NAME names no plugin folder ($dir)"
exit 1
fi
echo "dir=$dir" >> "$GITHUB_OUTPUT"
- uses: prusa3d/plugin-marketplace-publish@v1
with:
path: ${{ steps.plugin.outputs.dir }}
private-key: ${{ secrets.PRUSA_SLICER_PLUGIN_MARKETPLACE_SIGNING_KEY }}
The marketplace accepts a bundle only for a plugin id this repository and workflow are bound to, so one plugin’s release can never publish another. Each folder is a complete bundle: code shared between plugins has to be copied into every folder that uses it.
Inputs
| Input | Default | What it does |
|---|---|---|
private-key |
required | RSA private key, 2048 bits or more. Pass a secret. |
path |
. |
Folder with manifest.json and the files the plugin ships. |
marketplace-url |
production | The marketplace to publish to. |
channel |
auto | stable or beta. Empty: beta for a pre-release, stable otherwise. |
changelog |
release notes | Release notes, Markdown. Empty: the GitHub release body. |
check-version |
true |
For a tag, the tag must end with the version in manifest.json. |
dry-run |
false |
Build and sign only, do not upload. |
Security
- Anyone who can change the workflow can sign with the key in the secrets. Create a GitHub
environment with required reviewers that only
v*tags may deploy to, keep the secret in it, addenvironment: <name>to the job and the same environment to the trusted publisher. A workflow pushed on some branch then cannot read the key, every publish waits for an approval, and runs outside the environment are refused. - The first release remembers the repository’s numeric GitHub id, so a repository deleted and re-created under the same name by someone else cannot publish.
- The action ships plain files only: it refuses symbolic links and private keys in the plugin folder, and lists every file it signs in the run’s log.
When it fails
| The run says | What to do |
|---|---|
no OIDC token available |
Add permissions: id-token: write to the workflow. |
tag '...' does not match manifest.json version |
Bump version in manifest.json, or tag the release v<version> (or <folder>/v<version>). |
... is not a trusted publisher of ... |
The repository, workflow file or environment is not the one under Trusted publishing, or the plugin id is not added there. |
... is outside your namespaces |
Verify the domain, or use io.github.<your account>.*. |
signature did not verify against any registered key |
Register the public key of the key in the secret under Signing keys. |
bundle is signed with an expired key |
Put an active key in the secret, and its public key under Signing keys. |
version ... already exists |
This version is already uploaded. Bump the version for a new release. |
the answer did not come from the marketplace |
Something in front of the marketplace (a proxy or firewall on the way) refused the upload. The message names it, and a Cloudflare Ray ID if there is one; report it with that. |