This document is for maintainers of official module repositories. It describes the reusable module release workflow shipped by the SDK.
- Set
module.json'sversionto the intended exact SemVer version. - Build and commit every declared
dist/asset. - Run the module's lint, tests, frontend build, and package validation.
- Merge the release change, then create an unprefixed tag that exactly matches
module.json, for example1.2.3rather thanv1.2.3.
The reusable workflow rejects a tag that does not exactly match the manifest version. Stable releases require final SemVer; insider releases require a prerelease SemVer.
An official module normally calls the tagged SDK workflow from its own tag workflow:
jobs:
release:
uses: InvoiceShelf/modules/.github/workflows/module-release.yml@3.4.0
with:
channel: stable
secrets: inheritThe workflow installs dependencies, runs Pint and PHPUnit, builds frontend assets when present, validates the package, creates a timestamp-normalized ZIP, generates and validates release-manifest.json, signs its canonical JSON, and sends it to marketplace ingest.
Configure this protected GitHub environment in every official module repository:
| Setting | Type | Purpose |
|---|---|---|
MODULE_SIGNING_SECRET_KEY_B64 |
Secret | Standard-base64 raw Ed25519 secret key. |
MODULE_MARKETPLACE_INGEST_TOKEN |
Secret | Bearer token for the module's marketplace ingest request. |
MODULE_SIGNING_KEY_ID |
Variable | Identifier of the matching public signing key. |
MODULE_MARKETPLACE_INGEST_URL |
Variable | HTTPS ingest base URL ending in /api/marketplace/v1/modules. |
The workflow appends /{slug}/releases to the ingest URL. It disables persisted checkout credentials and uses the protected token only for that request.
Generate an Ed25519 keypair with:
vendor/bin/invoiceshelf-module generate-keypair official-modules-2026-09Official key ids follow official-modules-<yyyy>-<mm>. Put secret_key_b64 only in the environment secret, ideally piped straight from the command into gh secret set so it never appears on screen, and keep one encrypted copy (for example a sops file to the maintainer's age key) in the maintainer's password manager: GitHub cannot return a secret once stored, and a lost secret forces a key rotation. Configure public_key_b64 under the same key_id in both pinned maps, config/services.php in the website and config/invoiceshelf.php in InvoiceShelf, before the first release signed with it: the host refuses a release whose key_id it does not know, and installs only learn new keys through a host release (or the MARKETPLACE_PUBLIC_KEYS env override). Add new keys next to old ones; a key whose secret is lost stays trusted so releases already signed with it keep verifying. Discard the command output afterwards. Do not commit either key, the ingest token, or an organization-specific endpoint.
Before the first release of a new module, register it on the marketplace and provision its CI credential:
php artisan marketplace:module-register <slug> <ModuleName> "<Display name>" public InvoiceShelf --type=<type> --description="..." --dev=false
php artisan marketplace:module-credential provision <slug> --repository=InvoiceShelf/module-<slug>The credential is printed once as the last output line and belongs in MODULE_MARKETPLACE_INGEST_TOKEN; the repository it is bound to must equal the GitHub repository that runs the release workflow.
CI creates an immutable schema-v1 release-manifest.json containing the module identity/version, channel, compatibility, artifact SHA-256 and size, signing key ID, source commit, and release time. It signs canonical JSON with an Ed25519 detached signature.
The marketplace can mark a published release as yanked without re-signing it; package identity and artifact integrity remain signed. Use the SDK CLI to inspect a manifest locally:
vendor/bin/invoiceshelf-module validate-release release-manifest.json
vendor/bin/invoiceshelf-module canonicalize-release release-manifest.json > release-manifest.canonical.json