diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md new file mode 100644 index 00000000..505ddf3d --- /dev/null +++ b/.agents/skills/release/SKILL.md @@ -0,0 +1,200 @@ +--- +name: release +description: 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**: Semantic versioning (X.Y.Z), starting from 2.0.0 +- **Version source of truth**: `package.json` `version` field +- **Changelog**: `CHANGELOG.md` in repo root, using [Keep a Changelog](https://keepachangelog.com/) format +- **Version bumping**: Marketing-driven (not strict semver) + - **Patch (Z)**: Bug fixes, minor tweaks, dependency updates, small UI adjustments + - **Minor (Y)**: New user-facing features, significant UI changes, new pages/screens + - **Major (X)**: Only when the user explicitly requests it (milestones, rebrands, major redesigns) +- **CI trigger**: Pushing a semver 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: Pre-flight Checks + +```bash +# 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 2: Determine What Changed + +```bash +# 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 3: Decide the Version Bump + +Analyze the commits from Step 2 and determine the appropriate bump level: + +| Bump | When to use | Example | +|------|-------------|---------| +| **Patch** | Bug fixes, minor tweaks, dependency updates, small UI polish | 2.0.0 -> 2.0.1 | +| **Minor** | New user-facing features, new screens/pages, significant UI changes | 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. Choose minor if there are clearly new features. Never auto-bump major. + +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 4: Write the Changelog Entry + +Prepend a new section to `CHANGELOG.md` directly below the `# Changelog` heading. + +**Format:** + +```markdown +## [X.Y.Z] - YYYY-MM-DD + +### Added +- Description of new features + +### Changed +- Description of changes to existing features + +### Fixed +- Description of bug fixes + +### Removed +- Description of removed features +``` + +**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 + +### Step 5: Update Version in All Files + +Update the version string in these files: + +#### 5a. `package.json` + +Update the `version` field: + +```json +"version": "X.Y.Z" +``` + +#### 5b. `android/app/build.gradle` + +Update `versionName` (line 17). Do NOT change `versionCode` -- that is managed by CI: + +```groovy +versionName "X.Y.Z" +``` + +#### 5c. `ios/App/App.xcodeproj/project.pbxproj` + +Update `MARKETING_VERSION` in all 4 occurrences (2 Debug configs + 2 Release configs): + +``` +MARKETING_VERSION = X.Y.Z; +``` + +**Important:** There are exactly 4 lines containing `MARKETING_VERSION` in this file. All 4 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 6: Commit the Release + +```bash +git add package.json CHANGELOG.md android/app/build.gradle ios/App/App.xcodeproj/project.pbxproj +git commit -m "release: vX.Y.Z" +``` + +### Step 7: Tag the Release + +```bash +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 8: Push + +```bash +git push origin main --tags +``` + +This triggers the GitLab CI pipeline which will: +1. Build a signed Android APK and AAB +2. Create a GitLab Release with download links +3. Publish the APK to Zapstore + +### Step 9: 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 | +| `android/app/build.gradle` | `versionName` on line 17 | `versionCode` is managed by CI | +| `ios/App/App.xcodeproj/project.pbxproj` | `MARKETING_VERSION` (4 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 three jobs: + +1. **build-apk**: Builds signed Android APK and AAB, stamps `versionName` and `versionCode` into the build +2. **release**: Creates a GitLab Release with the changelog content and download links +3. **publish-zapstore**: Publishes the APK to Zapstore + +## 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: +```bash +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 3-8 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. diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4438ad1d..99e89434 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -46,7 +46,7 @@ build-apk: timeout: 15 minutes needs: [] rules: - - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-/ + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ variables: ANDROID_SDK_ROOT: /opt/android-sdk ANDROID_HOME: /opt/android-sdk @@ -97,8 +97,7 @@ build-apk: storeFile=my-upload-key.keystore EOF script: - # Extract version from git tag (e.g., v2026.03.16+974041a) - # versionName: full calver+sha string (e.g., 2026.03.16+974041a) + # Extract semver version from git tag (e.g., v2.1.0 -> 2.1.0) - TAG="${CI_COMMIT_TAG#v}" - VERSION_NAME="${TAG}" - VERSION_CODE="${CI_PIPELINE_IID}" @@ -152,13 +151,27 @@ release: needs: - build-apk rules: - - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-/ + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ script: - echo "Creating release for $CI_COMMIT_TAG" + # Extract the latest changelog section for the release description. + # Reads from "## [version]" to the next "## [" or end of file. + - | + VERSION="${CI_COMMIT_TAG#v}" + RELEASE_NOTES=$(awk "/^## \\[${VERSION}\\]/{found=1; next} /^## \\[/{if(found) exit} found{print}" CHANGELOG.md) + if [ -z "$RELEASE_NOTES" ]; then + RELEASE_NOTES="Ditto ${CI_COMMIT_TAG}" + fi + echo "RELEASE_NOTES<> release.env + echo "$RELEASE_NOTES" >> release.env + echo "ENDOFNOTES" >> release.env + artifacts: + reports: + dotenv: release.env release: tag_name: $CI_COMMIT_TAG name: $CI_COMMIT_TAG - description: "Ditto $CI_COMMIT_TAG" + description: $RELEASE_NOTES assets: links: - name: "Ditto-${CI_COMMIT_TAG}.apk" @@ -174,7 +187,7 @@ publish-zapstore: needs: - build-apk rules: - - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-/ + - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/ variables: SIGN_WITH: $ZAPSTORE_BUNKER_URL script: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5002cf19 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## [2.0.0] - 2026-03-26 + +Initial release of Ditto 2.0 -- a complete rewrite of Ditto. diff --git a/android/app/build.gradle b/android/app/build.gradle index a1d88ef3..975636f4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "pub.ditto.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 20260226 - versionName "2026.02.26" + versionCode 1 + versionName "2.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 9b0498cc..4865b1af 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -303,7 +303,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 2.0.0; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = pub.ditto.app; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -325,7 +325,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = pub.ditto.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/package-lock.json b/package-lock.json index 57abf7f1..d8f70734 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mkstack", - "version": "0.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mkstack", - "version": "0.0.0", + "version": "2.0.0", "dependencies": { "@capacitor/app": "^8.0.0", "@capacitor/core": "^8.1.0", diff --git a/package.json b/package.json index 58a3dd9f..b0ca84b0 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,14 @@ { "name": "mkstack", "private": true, - "version": "0.0.0", + "version": "2.0.0", "type": "module", "scripts": { "dev": "npm i --silent && vite", "build": "npm i --silent && vite build -l error && cp dist/index.html dist/404.html && echo 'Project built successfully!'", "test": "npm i --silent && tsc --noEmit && eslint && vitest run --reporter=dot --silent && vite build -l error && cp dist/index.html dist/404.html && echo 'All tests passed!'", "keygen": "keytool -genkey -v -keystore android/app/my-upload-key.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias upload", - "icons": "bash scripts/generate-icons.sh", - "release": "git tag \"v$(date +%Y.%m.%d)-$(git rev-parse --short HEAD)\" && git push origin --tags" + "icons": "bash scripts/generate-icons.sh" }, "engines": { "npm": "10.9.4", diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 37323e7c..51239e9e 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -171,6 +171,11 @@ export function SettingsPage() {
+ {/* Version footer */} +

+ v{import.meta.env.VERSION} ({new Date(import.meta.env.BUILD_DATE).toLocaleDateString()}) +

+ {/* Magic sigil — appears after 2 min inactivity, only when magic is locked */} {!config.magicMouse && sigilVisible && (