Files
eranos/.agents/skills/release/SKILL.md
T
Alex Gleason f5bb8afaec Run publish-app-store on the Mac runner instead of Linux
fastlane's deliver action invokes Apple's iTMSTransporter / altool to
push the IPA to App Store Connect, and those tools only ship inside
Xcode. On a generic ruby:3.3 Linux container the upload step crashed
with 'No such file or directory @ dir_chdir0' from
JavaTransporterExecutor#execute, because Helper.itms_path resolved
to a missing Xcode path.

Move publish-app-store onto the same self-hosted Mac runner as
build-ipa (tags: [macos]), drop the now-unnecessary 'gem install
fastlane' (the Mac has it on PATH via ~/.bash_profile), and unset
APP_STORE_CONNECT_API_KEY_PATH to mirror build-ipa's defense against
fastlane's env-var collision (match expects a JSON descriptor there;
we pass the API key inline via the Fastfile).

Update AGENTS.md and the release / ci-cd-publishing / mac-runner
skills, which all incorrectly described publish-app-store as a
Linux-only API call.

Regression-of: b8773c47
2026-05-11 14:00:13 -07:00

18 KiB

name, description
name description
release Publish a new app release with versioning, changelog, native build files, and git tagging. Triggered by "publish a new release" or similar requests.

Release Skill

This skill guides you through publishing a new release of the app. It handles version bumping, changelog generation, native build file updates, and git tagging/pushing.

Overview

  • Version format: Marketing version (X.Y.Z), starting from 2.0.0. This is NOT semver. Version numbers are chosen based on how the release looks to end users, not based on API compatibility or breaking changes. Think of it like an app store version -- the number reflects the perceived significance of the update to a regular user.
  • Version source of truth: package.json version field
  • Changelog: CHANGELOG.md in repo root, using Keep a Changelog format
  • Version bumping:
    • Patch (Z): Most releases. Bug fixes, tweaks, internal improvements, anything a user wouldn't specifically notice or seek out.
    • Minor (Y): Releases with headline features -- things worth announcing. A user should be able to look at the minor bump and think "oh, something new happened."
    • Major (X): Only when the user explicitly requests it (milestones, rebrands, major redesigns)
  • CI trigger: Pushing a version tag (v2.1.0) triggers the CI pipeline to build APKs, create a GitLab release, and publish to Zapstore

Release Procedure

Follow these steps in order. Do NOT skip any step.

Step 1: Required Reading

Before writing any release notes, you MUST read these pages to understand the product context, voice, and values:

  1. https://soapbox.pub/ -- Soapbox company overview and product suite
  2. https://soapbox.pub/ditto -- Ditto product page with feature descriptions and positioning
  3. https://about.ditto.pub/ -- Ditto documentation landing page
  4. https://about.ditto.pub/philosophy -- Ditto's design philosophy, core symbolism, and manifesto

These pages define what Ditto is, how it's positioned, and the tone of voice to use. Changelog entries should reflect this identity: fun, rebellious, user-focused, emphasizing freedom and self-expression. Avoid dry technical jargon -- write for people who use the app, not developers.

Step 2: Pre-flight Checks

# Ensure working directory is clean
git status

# Ensure we're on main branch
git branch --show-current

# Run the full test suite
npm run test
  • If the working directory has uncommitted changes, ask the user whether to commit them first or abort.
  • If not on main, warn the user and ask whether to proceed.
  • If tests fail, stop and fix the issues before continuing.

Step 3: Determine What Changed

# Get the current version from package.json
node -p "require('./package.json').version"

# Get commits since the last version tag
git log v$(node -p "require('./package.json').version")..HEAD --oneline
  • If there are no commits since the last tag, inform the user there is nothing to release and stop.
  • Review the commit list to understand the scope of changes.

Step 4: Decide the Version Bump

Analyze the commits from Step 3 and determine the appropriate bump level:

Bump When to use Example
Patch Bug fixes, minor tweaks, dependency updates, small UI polish, internal tooling, developer-facing pages, CI/build changes, settings/admin screens 2.0.0 -> 2.0.1
Minor Significant new product features that change how users interact with the app -- the kind of thing you'd highlight in an app store update or announce on social media (e.g., new content type support, DM redesign, new social features, theme system overhaul) 2.0.1 -> 2.1.0
Major ONLY when the user explicitly instructs a major bump 2.1.0 -> 3.0.0

Default to patch when in doubt. The bar for a minor bump is high -- ask yourself: "Would a regular user notice and care about this change?" If the answer is no, it's a patch. Internal pages (changelog, settings, about screens), infrastructure improvements, CI fixes, and developer tooling are always patch-level regardless of whether they technically add a new page or screen.

When bumping minor, reset patch to 0 (e.g., 2.0.3 -> 2.1.0). When bumping major, reset minor and patch to 0 (e.g., 2.3.1 -> 3.0.0).

Step 5: Write the Changelog Entry

Prepend a new section to CHANGELOG.md directly below the # Changelog heading.

Format:

## [X.Y.Z] - YYYY-MM-DD

A short single-paragraph summary of this release written in plain prose -- max 500 characters. This appears on the App Store, Google Play, and the in-app "what's new" toast.

### Added
- Description of new features

### Changed
- Description of changes to existing features

### Fixed
- Description of bug fixes

### Removed
- Description of removed features

The Summary Paragraph

Every release section MUST start with a single plaintext paragraph (not a bullet, not a heading) that summarises the release for app-store-style audiences:

  • Single paragraph, plain prose. No bullets, no headings, no Markdown formatting beyond plain text.
  • Max ~500 characters. Apple App Store and Google Play both cap "What's new" text at 500. The CI release-notes job warns when the summary is longer.
  • Audience: end users discovering the update. Describe the most noticeable user-visible changes; omit internal cleanups even if they're in the bullets below.
  • Tone matches the bullets. Present-tense, no Nostr jargon, no NIP/kind numbers (see Rules below).
  • Maintenance releases -- write a one-sentence summary like A behind-the-scenes maintenance release with no user-facing changes. Don't leave it blank; the CI fallback Ditto vX.Y.Z is a last resort for legacy entries, not new ones.

The same paragraph is used in three places automatically:

  • App Store -- "What's New in This Version" via fastlane deliver
  • Google Play -- "What's new in this version" via fastlane supply metadata/android/<lang>/changelogs/<versionCode>.txt
  • In-app toast -- the What's new in vX.Y.Z toast that fires when users load a new version (see src/components/VersionCheck.tsx)
  • The full section (summary + lists) goes into the GitLab Release description.

Extraction is handled by scripts/extract-release-notes.mjs; you don't have to write store-specific copy.

Changelog Quality Checklist

Before drafting any entries, run through this checklist. It is NOT optional -- skipping steps here is the most common way a release goes out with misleading notes.

5.1. Diff the code, not just the commit log

Commit messages describe intent at the moment of commit; they over- and under-represent the cumulative effect at release time. Before drafting entries, run a real diff for each area of substantial change:

# Full diff between tags
git diff v<prev>..HEAD

# Or narrowed to an area you're unsure about
git diff v<prev>..HEAD -- src/components/ComposeBox.tsx

Only the diff reveals intra-release churn (commits that cancel each other out, bugs introduced and then fixed, refactors that land and get reverted). Reading commit messages alone is insufficient.

5.2. Trace every candidate "Fixed" entry to its origin commit

For each bug fix you're considering listing, find the commit that introduced the bug.

Fast path -- check for Regression-of: trailers (see AGENTS.md "Attributing Regressions"). If the fix commit declares its origin in a trailer, you don't need to hunt:

# List all commits in the release window with their Regression-of trailers (if any)
git log v<prev>..HEAD --no-merges \
  --format='%h %s%n  Regression-of: %(trailers:key=Regression-of,valueonly,separator=%x20)'

For each Regression-of: <sha> entry, check whether <sha> is also in the release window:

# Returns 0 if <sha> is BEFORE v<prev> (pre-existing bug -> legit "Fixed" entry)
# Returns non-zero if <sha> is AFTER v<prev> (intra-release -> omit from "Fixed")
git merge-base --is-ancestor <sha> v<prev>

Fallback -- manual tracing (when no trailer is present):

# Show the history of a file across all commits
git log --oneline v<prev>..HEAD -- path/to/file.tsx

# Or blame the specific lines the fix touched
git blame -L <start>,<end> -- path/to/file.tsx

If the introducing commit is also in this release window (i.e. after the previous tag), the bug is intra-release. The user on the previous version never experienced it. Do NOT list it as a "Fixed" entry. Fold it into the relevant "Added" or "Changed" entry, or omit it entirely.

5.3. The "Would a user on the previous version notice this?" test

The changelog describes the delta between the previous release and this one from the user's perspective -- not the development history. Before writing each entry, ask:

"Did a user on the previous published version experience this exact thing?"

  • If they experienced a broken state that is now fixed: "Fixed" entry
  • If they experienced the old behavior and now see new behavior: "Changed" or "Added" entry
  • If they never saw either state (introduced AND resolved within this release window): omit entirely

This applies to more than just bugs:

  • A feature added and then reverted in the same release: omit both
  • A refactor that was done and then undone: omit both
  • A performance regression introduced and then fixed: omit both
  • A typo introduced in a new string and then corrected: mention the new string (if user-facing) as a single "Added"/"Changed" entry, with no "Fixed" entry
5.4. Worked example -- intra-release bug

Scenario: Commit A overhauls the compose box and, as a side effect, breaks the background of the expanded emoji picker. Commit B, later in the same release window, restores the background.

Correct changelog: One "Added" entry describing the compose box overhaul. The emoji picker background is part of the finished state the user receives.

Incorrect changelog: An "Added" entry for the overhaul AND a "Fixed" entry for the emoji picker background. The user on the previous version never saw the broken background; listing it invents a problem they didn't have and makes the release notes read like a developer changelog.

Rules

  • Only include categories that have entries (omit empty categories)
  • Write user-facing descriptions, not raw commit messages
  • Keep descriptions concise -- one line per change
  • Group related commits into single entries where appropriate
  • Use present tense ("Add dark mode toggle", not "Added dark mode toggle")
  • Focus on what the user sees/experiences, not internal implementation details
  • Use the current date in YYYY-MM-DD format
  • Never use Nostr protocol jargon. NIP numbers (e.g., "NIP-89", "NIP-17"), kind numbers (e.g., "kind 30078"), and other protocol-level references must not appear in the changelog. Describe the feature in plain language from the user's perspective. For example, write "App cards for Nostr apps" instead of "App cards for Nostr apps (NIP-89)". The changelog audience is end users, not protocol developers.
  • Only ship what the user sees. If a bug was introduced AND fixed within this release, the user never saw it -- omit the fix entirely (or fold the net result into the relevant Added/Changed entry). The same applies to features that were added and reverted, refactors that cancel out, and any other intra-release churn. See the Changelog Quality Checklist above (especially 5.2 and 5.3) for the procedure to verify this.
  • Collapse related work into one entry. If a feature was added and then tweaked across multiple commits in the same release, present the finished result as a single "Added" entry. Never list something as "Added" and then also list fixes for that same thing -- the user sees the end product, not the development history.
  • Omit purely internal changes. CI fixes, build pipeline tweaks, developer tooling, and infrastructure changes should be omitted from the changelog entirely unless they have a direct, visible impact on the user experience. The changelog is for users, not developers.

Step 6: Update Version in All Files

Update the version string in these files:

6a. package.json

Update the version field:

"version": "X.Y.Z"

6b. android/app/build.gradle

Update versionName (line 17). Do NOT change versionCode -- that is managed by CI:

versionName "X.Y.Z"

6c. ios/App/App.xcodeproj/project.pbxproj

Update MARKETING_VERSION in all occurrences (Debug + Release configs):

MARKETING_VERSION = X.Y.Z;

Important: All lines containing MARKETING_VERSION must be updated to the same value. Use a replaceAll operation.

Do NOT change CURRENT_PROJECT_VERSION -- it stays at 1 (may be managed separately for App Store submissions in the future).

Step 7: Copy Changelog to Public Directory

The changelog is served at runtime by the app from the public/ directory. After updating CHANGELOG.md, copy it:

cp CHANGELOG.md public/CHANGELOG.md

Step 8: Pull Latest Changes

Before committing the release, pull the latest changes from the remote to ensure the release commit sits on top of the latest code. This must happen before committing and tagging.

git pull origin main

CRITICAL: Always use git pull (merge), NEVER git pull --rebase. Rebasing rewrites commit hashes, which would orphan any tag pointing to the original commit. Since version tags are often protected on the remote and cannot be deleted or updated, a broken tag cannot be easily fixed.

If there are merge conflicts with the pulled changes, resolve them before proceeding.

Step 9: Commit the Release

git add package.json CHANGELOG.md public/CHANGELOG.md android/app/build.gradle ios/App/App.xcodeproj/project.pbxproj
git commit -m "release: vX.Y.Z"

Step 10: Tag the Release

git tag vX.Y.Z

The tag format is v followed by the semver version with no suffix. Examples: v2.0.0, v2.1.0, v2.1.1.

Step 11: Push

git push origin main vX.Y.Z

CRITICAL: Push only the specific tag being released. NEVER use --tags -- that pushes ALL local tags, including stale or deleted ones.

This triggers the GitLab CI pipeline which will:

  1. Build a signed Android APK and AAB
  2. Build a signed iOS IPA on the self-hosted Mac runner
  3. Extract release notes (full body + summary paragraph) from CHANGELOG.md
  4. Create a GitLab Release with APK / AAB / IPA download links
  5. Publish the APK to Zapstore
  6. Publish the AAB to Google Play (production track) with the summary as the "What's new" text
  7. Submit the iOS IPA to App Store Connect for review with the summary as the "What's New" text

Step 12: Confirm

After pushing, inform the user:

  • The new version number
  • A brief summary of what was released
  • That CI will handle building and publishing the artifacts

File Reference

File What to update Notes
package.json version field Source of truth for the version
CHANGELOG.md Prepend new section User-facing changelog
public/CHANGELOG.md Copy from CHANGELOG.md Served at runtime by the app
android/app/build.gradle versionName on line 17 versionCode is managed by CI
ios/App/App.xcodeproj/project.pbxproj MARKETING_VERSION (all occurrences) CURRENT_PROJECT_VERSION stays at 1

CI Pipeline

The CI pipeline (.gitlab-ci.yml) is triggered by tags matching the pattern /^v\d+\.\d+\.\d+$/ (e.g., v2.1.0). It runs seven jobs:

  1. build-apk: Builds signed Android APK and AAB, stamps versionName and versionCode into the build
  2. build-ipa: Builds the signed App Store IPA on the self-hosted Mac runner (tags: [macos]); stamps MARKETING_VERSION and CFBundleVersion into the Xcode project. The IPA is uploaded to GitLab's Generic Packages registry and exposed as a CI artifact for downstream jobs
  3. release-notes: Extracts the version's changelog section and summary paragraph from CHANGELOG.md into two artifacts (release-notes.md and release-notes-summary.txt) consumed by release, publish-app-store, and publish-google-play
  4. release: Creates a GitLab Release with the full changelog section and APK / AAB / IPA download links
  5. publish-zapstore: Publishes the APK to Zapstore
  6. publish-google-play: Uploads the AAB to Google Play production track and writes the release summary to metadata/android/en-US/changelogs/<versionCode>.txt
  7. publish-app-store: Submits the prebuilt IPA to App Store Connect for review with the release summary as the "What's New" text. Runs on the self-hosted Mac runner (tags: [macos]) because fastlane deliver shells out to Apple's iTMSTransporter to upload the IPA, and that tool only ships inside Xcode — the previous Linux runner crashed at the upload step with No such file or directory @ dir_chdir0 because Helper.itms_path resolved to a missing Xcode path. The build appears in App Store Connect within ~30 minutes; Apple's human review then takes 24-48 hours typically. Once approved, you must release manually in App Store Connect (automatic_release: false) — this is the final human gate. For runner operations, match cert rotation, and debugging, load the mac-runner skill.

Troubleshooting

"Nothing to release"

If git log shows no commits since the last tag, there genuinely is nothing to release.

Tests fail

Fix the failing tests before proceeding. The release must not contain broken code.

Wrong version bumped

If you tagged the wrong version and haven't pushed yet:

git tag -d vX.Y.Z          # delete the local tag
git reset --soft HEAD~1     # undo the commit but keep changes staged

Then redo steps 4-10 with the correct version.

Already pushed a bad release

This requires manual intervention. Inform the user and suggest they delete the tag and release from GitLab manually, then re-run the release process.