Repository navigation
Releasing
The free-plugin release (.github/workflows/release-tarballs.yml) compiles a command binary for every
plugin that ships one, for five platforms. Which binaries, and which commands they carry, is not read
from plugin.json by the workflow. scripts/release/cli-targets.sh DIR prints them, one
binary<TAB>command line each, from the cli tool manifestv2migrate -targets.
- The tool normalizes a v1 or v2
plugin.jsonand applies the installer's own rule (manifestv2.CLITargets, the same functionnself adduses to decide which binaries to link), so the release builds exactly what the installer will look for. -
scripts/release/cli-targets-gate.shruns over every free plugin first and decides what may ship. It blocks a plugin whose commands include a core verb or top-level command of the pinned cli (read from that commit's.github/command-registry.json), a name outside^[a-z][a-z0-9-]*$, a binary another plugin also yields, or a manifest the normalizer refuses. Each blocked plugin needs an unexpired entry (rulecli-targets, owner_ref naming the fixing Ticket, expiry within the v1.5.0 cap) inscripts/conformance/exceptions.yaml, else the run fails. Blocked plugins ship no binary and are listed in the job summary. - A plugin with no command prints nothing and exits 0. A manifest the tool refuses exits 1; the gate turns that into a block that needs an exception.
- The tool comes from the cli commit pinned in
scripts/cli-tools.version. The workflow builds it once (Go, from that commit's vendor directory) and shares it with the parallel plugin builds. Changing the pin is the way to pick up a new rule. - Fixtures:
tests/fixtures/manifest-v1-cli/and its v2 twintests/fixtures/manifest-v2-cli/must print identical targets.
A release tag (v*) or a manual run of release-tarballs.yml with a tag input runs four jobs. Each holds only what it needs, and
the chain uses the workflow's own GITHUB_TOKEN plus scoped repository secrets: no personal access token and no cross-repository trigger.
| Job | Permissions | Secrets | Does |
|---|---|---|---|
build |
contents read | none | deterministic source and per-platform tarballs (scripts/deterministic-tar.sh) |
publish |
contents write, id-token write, attestations write | Cloudflare R2 (R2 step only) | uploads missing tarballs to the Release (never --clobber), re-downloads the published bytes, writes the .sha256 files from them, mirrors those bytes to R2 and reads each object back, attests build provenance for the tarballs this run built, records the published sha256 list (also in the job summary) |
sign |
contents read | free-tier signing key (signing step only) | bound to the release-signing Environment; downloads what the Release serves, refuses it unless it equals the list publish recorded, signs the source tarballs; runs the tag's own commit of sign.sh
|
release-data |
contents write, pull-requests write | none | writes releases.json from the published bytes and the signatures, opens the release-data PR |
Checksums and signatures always describe the bytes on the Release, never a rebuild. Gzip output differs between machines, so an asset that is already on the Release stays the artifact of record and everything downstream follows it.
- Message:
nself-plugin-v1\n<slug>\n<version>\n<sha256 hex of the source tarball>\n, signed with raw Ed25519 (openssl pkeyutl -sign -rawin). -
release_signatureinreleases.jsonand the projections:{"key_id": "plugins-<16 hex>", "alg": "ed25519", "sig": "<base64>"}.sigis the 64-byte signature in standard padded base64 (the textsdk/go/signingEncodeSigproduces);verify-signatures.shaccepts only that canonical text.key_idis derived, never chosen:plugins-plus the first 16 hex of sha256 over the raw 32-byte public key.sign.shrefuses a key id that does not match its key. - Only the source tarball
<slug>-<version>.tar.gzis signed. The per-platform binary tarballs are covered byplatform_checksums(in the unsignedreleases.json) and by the provenance attestation; verify them withgh attestation verify <file> --repo nself-org/plugins. Only bytes this run built and the Release still serves unchanged are attested: a tarball uploaded earlier (a draft upload, an older run) is published and checksummed but carries no attestation, and thepublishjob summary says which.verify-signatures.shcounts source tarballs (checksum + signature) and binary tarballs (checksum only) separately. - A source tarball is signed only when its plugin's manifest
versionequals the tag's version; the signed set is exactly the set of rowsrelease-datawrites. Both scripts accept Ed25519 keys only and refuse anything else. - Verification order is checksum, then signature, both before extraction.
| Script | Use |
|---|---|
sign.sh SLUG VERSION TARBALL |
prints the release_signature JSON. Key from NSELF_RELEASE_SIGNING_KEY_FREE (PKCS8 PEM) and NSELF_RELEASE_SIGNING_KEY_FREE_ID, environment only; written to a 0600 temp file removed on exit |
verify-signatures.sh --pub FILE DIR |
checks every *.tar.gz in DIR against DIR/releases.json and exactly the public key FILE (PEM SPKI). Failures on stderr: checksum mismatch, missing release data, missing tarball, unsigned, signature mismatch. No key discovery |
rehearse.sh --tag T --no-upload --out DIR |
builds all source tarballs and signs them with a throwaway key; writes DIR/releases.json and DIR/rehearsal-test.pub. Two runs of one tag give byte-identical tarballs |
keys/ |
committed release public keys (plugins-release-free-<id>.pub). Empty until P7-PLUG-69 commits the production key, which must happen before the first real tag run |
OpenSSL 3 is required (OPENSSL=/path/to/openssl to pick one); macOS system LibreSSL cannot sign or verify Ed25519 with -rawin.
GNU tar is required for deterministic tarballs (brew install gnu-tar).
bash scripts/release/rehearse.sh --tag v0.0.0-rehearsal --no-upload --out "$TMPDIR/rehearsal"
bash scripts/release/verify-signatures.sh --pub "$TMPDIR/rehearsal/rehearsal-test.pub" "$TMPDIR/rehearsal"The same directory fails against any other key (signature mismatch, including every committed key in keys/) and a one-byte change in any
tarball fails with checksum mismatch. A check that only compared sha256 to releases.json would pass for any key; this one does not.
The Ticket P7-PLUG-21 verification lines are the gate for these scripts; no workflow runs them yet.
- The free-tier key is an Actions secret of the
release-signingEnvironment on this repository. The Environment must have a required reviewer who is not the person who started the run (prevent self-review), a custom deployment policy that allows tagsv*only, and hold no other signing key. A tag ruleset that limits creating, updating and deletingv*tags to maintainers belongs with it, because the tag decides which code runs next to the key. The owner creates the Environment and both secrets (NSELF_RELEASE_SIGNING_KEY_FREE,NSELF_RELEASE_SIGNING_KEY_FREE_ID) in P7-PROD-59 and checks those settings; this page describes what must be true, not what is configured today. No other job can read the secrets. - The licensed key is never a GitHub secret and never available to this workflow; it stays on the owner's release machine (P7-PLUG-22).
- Rotation: generate the new key offline, commit its public half as
keys/plugins-release-free-<id>.pub(thesignjob refuses a key that is not committed), then store it as the Environment secrets, and ship a CLI that pins the new key id before the first release signed with it (P7-PLUG-71). Keep the old public key until every supported CLI knows the new one. A leaked key goes on the CLI's revoked list instead. - The private key never appears in a log, an artifact, a file in the repo or an argv.
- A release runs from the tag push. A manual run (
workflow_dispatch) must start from the tag itself ("Use workflow from: Tag v1.2.3") and enter that same tag; the first step refuses anything else, so every job runs the tag's code and the concurrency group is the tag. The Release and the R2 mirror exist before the reviewer approvessign; trust travels inreleases.json, which only changes after the signatures exist. - The
signjob refuses to run unlesskeys/holds a release public key and the signatures verify against one of them, so a mis-set secret cannot produce a signed-looking PR. Commit the public key (P7-PLUG-69) before the first real tag run. - A manual run from an older tag runs that tag's own workflow file, so this chain cannot backfill releases made before it.
- Approve the
signjob in the Actions UI (the required reviewer). - Check out the
release-data/<tag>branch on a maintainer machine that has the private bundles checkout and runbash scripts/plugin-counts.sh: it regeneratesregistry.json,catalog.jsonandcounts.jsonfrom the newreleases.json, and the workflow token cannot read the bundles repo. Push the result to the branch. That push also starts the required checks, which a pull request opened withGITHUB_TOKENdoes not. Then merge. A re-run never overwrites an existingrelease-data/<tag>branch; delete it first to regenerate.verify-published-checksums.sh --signaturesrefuses to write a row over a newer version, so a late run for an older tag cannot rollreleases.jsonback. - The web
sources-lockPR is a separate step (P7-PLUG-69). - The repository setting "Allow GitHub Actions to create and approve pull requests" (Settings, Actions, General) must stay on; the
release-datajob opens its PR withGITHUB_TOKEN.
- Commands
- File Processing Commands
- GitHub Commands
- ID.me Commands
- Jobs Commands
- Notifications Commands
- Realtime Commands
- Shopify Commands
- Stripe Commands
View All: Home (or see the full alphabetical list below — 129/129 synced with registry.json)
- Access-Controls
- Admin-Api
- AI-CLI
- AI-Studio
- Alerts
- Analytics
- API
- Audit
- Audit-Analytics
- Audit-Log
- Auth-Enterprise
- Backup
- BYOK
- CDC
- CDN
- CI
- Claw-CLI
- Cloudflare
- Compliance
- Content-Acquisition
- Content-Progress
- Content-Safety
- Costs
- CRDT
- Cron
- DDNS
- Devices
- DLQ
- Documents
- Dogfood
- Donorbox
- DR
- E2EE
- Encryption
- Entitlements
- Event-Bus
- Family-Ancestry
- Family-FamilySearch
- Family-GEDCOM
- Family-MyHeritage
- Family-WikiTree
- Feature-Flags
- Federation
- File-Processing
- Flags
- Forgejo
- Functions-V8
- Game-Metadata
- Gateway
- Gauth
- GDPR
- Geocoding
- GitHub
- GitHub-Runner
- HIPAA
- Home
- IDme
- Infra
- Invitations
- Job-Queue
- Jobs
- K8s
- Link-Preview
- Maintenance
- MDNS
- Media-Processing
- Meetings
- MLflow
- Model
- Monitor
- Monitoring
- Notifications
- Notify
- nSelf-Cloud
- nSelf-Eval-Gate
- nSelf-Geo
- nSelf-Image
- nSelf-PDF
- nSelf-Scan
- nSelf-Sync
- nSelf-Vault
- Object-Storage
- Observability
- Ollama
- Payments
- PayPal
- Pentest
- Pentest-Kit
- Plugin-ClawDE
- Plugin-Gauth
- Plugin-LLM-Gateway
- Plugin-PTY
- Plugin-Retrieval
- Podcast
- Post
- Push
- Queue
- Region
- Release
- Retro-Gaming
- Rom-Discovery
- Search
- Sentry-CLI
- Shared-Utils
- Shopify
- SIEM
- SMS
- Soak
- Sports
- Storage
- Stripe
- Subtitle-Manager
- Tenant
- Tenant-Controller
- TMDB
- Tokens
- Torrent-Manager
- Traefik
- Transactional-Email
- VPN
- WAF
- Watchdog
- Web3
- Webhooks
- Workflows
Related