Extract eleven topic areas into loadable skills so AGENTS.md can serve as a scannable overview instead of a specification dump. The file shrinks from 1480 to 358 lines (~76%) while keeping every concrete rule, critical code pattern, and pointer that an agent needs on first read. New Ditto-specific skills: - nostr-kinds: NIP-vs-custom-kind decision framework, kind ranges, tag design, content-vs-tags, NIP.md update rule, and Ditto's seven-location UI registration checklist for new kinds (NoteCard, PostDetailPage, extraKinds.ts, KIND_LABELS/KIND_ICONS in CommentContext, WELL_KNOWN_KIND_LABELS in ExternalContentHeader, EmbeddedNote/EmbeddedNaddr, ReplyComposeModal). - nostr-publishing: useNostrPublish, the read-modify-write pattern via fetchFreshEvent + prev for replaceable/addressable events, published_at contract, and d-tag collision prevention. - nostr-queries: the standard useNostr + useQuery pattern, combining kinds into one filter to avoid rate limits, and the NIP-52 validator walkthrough. - theming: @fontsource install flow, the Ditto runtime font-loader path (sanitizeUrl + sanitizeCssString), color scheme variables, useTheme toggle, and the isolate + negative-z-index gotcha. - ci-cd-publishing: Zapstore NIP-46 bunker auth (zsp + nip46-auth.mjs), nsite deploys (nsyte nbunksec + configured relays/servers), and Google Play AAB uploads via fastlane supply (service-account JSON base64 encoding and rotation). - capacitor-compat: WKWebView/WebView limitations, the downloadTextFile / openUrl helpers in src/lib/downloadFile.ts, platform detection, and the full plugin list. - git-workflow: pre-commit validation order and the Regression-of: trailer convention used by the release skill's changelog generator. Ported from mkstack, lightly adapted where needed: - nip19-routing: root-level /:nip19 routing and filter construction patterns (adapted to reference Ditto's existing NIP19Page). - nostr-relay-pools: nostr.relay() and nostr.group() for targeted queries. - nostr-encryption: NIP-44 / NIP-04 via the user's signer. - file-uploads: useUploadFile + Blossom + NIP-94 imeta tag construction. AGENTS.md itself now follows mkstack's density — concrete rules inline, one code example per section, pointer to the matching skill for details. The enumerations that previously bloated it (every shadcn primitive, every hook, every Capacitor plugin, the full NostrMetadata type dump, the NIP-19 prefix reference table, etc.) are either removed in favor of "ls the directory" or moved into their skill.
7.9 KiB
name, description
| name | description |
|---|---|
| ci-cd-publishing | Ditto's release and publishing pipeline — cutting a version tag, Zapstore APK publishing with NIP-46 bunker auth, nsite web deploys via nsyte, and Google Play AAB uploads via fastlane supply. Includes GitLab CI variable setup and credential rotation. |
CI/CD Pipeline and Publishing
Ditto uses GitLab CI (.gitlab-ci.yml) to run tests on every commit, deploy the web app to nsite on every default-branch push, and build + publish Android binaries to Zapstore and Google Play on every tag. Load this skill when setting up CI credentials, rotating a signing key, diagnosing a failed publish, or adding a new publishing target.
Pipeline Overview
| Stage | Runs on | Job |
|---|---|---|
test |
every commit (not tags) | npm run test |
deploy |
default branch only | deploy-nsite (Vite build → nsyte) |
build |
tags only | build-apk (signed release APK + AAB) |
release |
tags only | GitLab Release with APK artifact |
publish |
tags only | publish-zapstore + publish-google-play |
Creating a Release
Releases are triggered by pushing a version tag:
npm run release
This creates a tag in the format v2026.03.14+abc1234 (date + short commit hash) and pushes it to GitLab, which triggers the build-apk, release, publish-zapstore, and publish-google-play jobs.
For the full versioning / changelog / native-build workflow, load the release skill.
Zapstore Publishing
The publish-zapstore CI job uploads signed APKs to Zapstore using the zsp CLI and NIP-46 bunker signing via Amber.
Configuration files:
zapstore.yaml— app metadata for Zapstore (name, tags, icon, supported NIPs).gitlab-ci.yml— thepublish-zapstorejob definition
GitLab CI/CD variables (Settings → CI/CD → Variables):
| Variable | Description | Protected | Masked | Raw |
|---|---|---|---|---|
ZAPSTORE_BUNKER_URL |
NIP-46 bunker URL (bunker://<pubkey>?relay=...). No secret param needed after initial auth. |
Yes | No | Yes |
ZAPSTORE_CLIENT_KEY |
Hex private key used as the NIP-46 client identity for bunker communication | Yes | Yes | Yes |
ANDROID_KEYSTORE_BASE64 |
Base64-encoded Android signing keystore | Yes | Yes | Yes |
KEYSTORE_PASSWORD |
Android keystore password | Yes | Yes | Yes |
KEY_PASSWORD |
Android key password | Yes | Yes | Yes |
How NIP-46 bunker auth works in CI
NIP-46 bunker signing requires two keys: the user's key (held by Amber) and a client key (the CI runner's identity). The bunker authorizes specific client pubkeys — once authorized, the client can request signatures without re-approval.
The publish-zapstore job restores the client key from ZAPSTORE_CLIENT_KEY into ~/.config/zsp/bunker-keys/<bunker-pubkey>.key before running zsp, so the bunker recognizes the CI runner as an already-authorized client.
Initial setup (one-time)
Run the NIP-46 client-initiated auth script:
node scripts/nip46-auth.mjs
This generates a nostrconnect:// URI. Import/paste it into Amber and approve the connection. The script outputs the bunker:// URI and client key hex, and writes the client key to ~/.config/zsp/bunker-keys/. Update the GitLab CI/CD variables with the printed values.
Options:
--relay <url>— relay for NIP-46 communication (default:wss://relay.ditto.pub)--name <name>— app name shown to the signer (default:Ditto)--timeout <sec>— how long to wait for approval (default: 300)
After authorization, the bunker recognizes the client key and no secret or manual approval is needed for CI runs. If the client key is rotated, run the script again and update the GitLab variables.
nsite Publishing
The deploy-nsite CI job deploys the Vite build to nsite on every push to the default branch using nsyte. The job uploads dist/ to Blossom servers and publishes site manifest events to Nostr relays.
nsyte uses a NIP-46 bunker credential called nbunksec — a bech32-encoded string bundling the bunker pubkey, client secret key, and relay info into a single self-contained token. It's passed to nsyte via --sec.
GitLab CI/CD variables:
| Variable | Description | Protected | Masked | Raw |
|---|---|---|---|---|
NSITE_NBUNKSEC |
nbunksec credential from nsyte ci. Must start with nbunksec1. |
Yes | Yes | Yes |
Initial setup (one-time)
- Install nsyte locally:
curl -fsSL https://nsyte.run/get/install.sh | bash - Generate the CI credential:
This guides you through connecting a NIP-46 bunker (e.g. Amber) and outputs an
nsyte cinbunksec1...string. The credential is shown only once. - Add the
nbunksec1...value asNSITE_NBUNKSECin GitLab CI/CD settings. Mark it as Protected and Masked.
Configured relays and servers
Relays the deploy job publishes to:
wss://relay.ditto.pubwss://relay.nsite.lolwss://relay.dreamith.towss://relay.primal.net
Blossom servers:
https://blossom.primal.nethttps://blossom.ditto.pubhttps://blossom.dreamith.to
The --use-fallback-relays and --use-fallback-servers flags include nsyte's built-in defaults for broader coverage. The --fallback "/index.html" flag enables SPA client-side routing.
Credential rotation
To rotate the nsite credential:
- Revoke the old bunker connection in your signer app.
- Run
nsyte ciagain to generate a newnbunksec1...string. - Update the
NSITE_NBUNKSECvariable in GitLab CI/CD settings.
Google Play Publishing
The publish-google-play CI job uploads Android AABs to Google Play using fastlane supply. It runs after a successful AAB build and uploads directly to the production track.
GitLab CI/CD variables:
| Variable | Description | Protected | Masked | Raw |
|---|---|---|---|---|
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON |
Base64-encoded contents of the Google Play API service account key JSON. The CI job decodes with base64 -d before passing to fastlane supply. |
Yes | Yes | No |
Initial setup (one-time)
-
Create or reuse a project in Google Cloud Console.
-
Enable the Google Play Developer API for that project.
-
In Google Cloud Console, go to Service Accounts, create a service account, and download a JSON key file for it.
-
In Google Play Console, go to Users & Permissions, click Invite new users, enter the service account email, and grant it permission to manage releases for
pub.ditto.app. -
Base64-encode the key file:
# Linux base64 -w0 service-account.json # macOS base64 -i service-account.json | tr -d '\n' -
Add the base64-encoded value as
GOOGLE_PLAY_SERVICE_ACCOUNT_JSONin GitLab CI/CD settings. Mark it as Protected and Masked. Do not paste the raw JSON — the CI script expects base64 and will fail to decode a raw value.
Key points
- The job uploads the signed AAB (not APK) — Google Play requires App Bundles.
- Uploads go directly to the production track. Google's review process still applies before the update reaches users.
- Metadata, screenshots, and changelogs are managed in the Play Console, not via CI (the job uses
--skip_upload_metadataetc.). - The same signing keystore used for Zapstore is reused here (
ANDROID_KEYSTORE_BASE64,KEYSTORE_PASSWORD,KEY_PASSWORD).