Files
eranos/.agents/skills/ci-cd-publishing/SKILL.md
T
Alex Gleason bd68a32708 Split AGENTS.md into skills; compress to 358 lines
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.
2026-04-26 23:13:30 -05:00

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 — the publish-zapstore job 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)

  1. Install nsyte locally:
    curl -fsSL https://nsyte.run/get/install.sh | bash
    
  2. Generate the CI credential:
    nsyte ci
    
    This guides you through connecting a NIP-46 bunker (e.g. Amber) and outputs an nbunksec1... string. The credential is shown only once.
  3. Add the nbunksec1... value as NSITE_NBUNKSEC in GitLab CI/CD settings. Mark it as Protected and Masked.

Configured relays and servers

Relays the deploy job publishes to:

  • wss://relay.ditto.pub
  • wss://relay.nsite.lol
  • wss://relay.dreamith.to
  • wss://relay.primal.net

Blossom servers:

  • https://blossom.primal.net
  • https://blossom.ditto.pub
  • https://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:

  1. Revoke the old bunker connection in your signer app.
  2. Run nsyte ci again to generate a new nbunksec1... string.
  3. Update the NSITE_NBUNKSEC variable 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)

  1. Create or reuse a project in Google Cloud Console.

  2. Enable the Google Play Developer API for that project.

  3. In Google Cloud Console, go to Service Accounts, create a service account, and download a JSON key file for it.

  4. 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.

  5. Base64-encode the key file:

    # Linux
    base64 -w0 service-account.json
    
    # macOS
    base64 -i service-account.json | tr -d '\n'
    
  6. Add the base64-encoded value as GOOGLE_PLAY_SERVICE_ACCOUNT_JSON in 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_metadata etc.).
  • The same signing keystore used for Zapstore is reused here (ANDROID_KEYSTORE_BASE64, KEYSTORE_PASSWORD, KEY_PASSWORD).