Add release system with semver versioning, changelog, and release skill

- Switch from CalVer (date+SHA) to semantic versioning starting at v2.0.0
- Create release skill (.agents/skills/release/) with full AI-guided release workflow
- Add CHANGELOG.md with initial 2.0.0 entry
- Update CI tag regex to match semver tags (v2.0.0 instead of v2026.03.24-sha)
- Extract changelog content into GitLab release descriptions
- Update Android versionName to 2.0.0 in build.gradle
- Update iOS MARKETING_VERSION to 2.0.0 in pbxproj
- Expose VERSION (semver) and BUILD_DATE (ISO 8601) as build-time constants
- Display version and build date in Settings page footer
- Remove npm release script (releases are now done via the AI skill)
This commit is contained in:
Alex Gleason
2026-03-26 21:13:32 -05:00
parent 1dbbaa283c
commit cbbb576b26
10 changed files with 246 additions and 27 deletions
+200
View File
@@ -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.
+19 -6
View File
@@ -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<<ENDOFNOTES" >> 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:
+5
View File
@@ -0,0 +1,5 @@
# Changelog
## [2.0.0] - 2026-03-26
Initial release of Ditto 2.0 -- a complete rewrite of Ditto.
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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 = "";
+2 -2
View File
@@ -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",
+2 -3
View File
@@ -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",
+5
View File
@@ -171,6 +171,11 @@ export function SettingsPage() {
<div className="h-px flex-1 bg-gradient-to-l from-transparent via-primary/20 to-primary/30" />
</div>
{/* Version footer */}
<p className="text-center text-[11px] text-muted-foreground/50 select-none pt-1 pb-2">
v{import.meta.env.VERSION} ({new Date(import.meta.env.BUILD_DATE).toLocaleDateString()})
</p>
{/* Magic sigil — appears after 2 min inactivity, only when magic is locked */}
{!config.magicMouse && sigilVisible && (<div className="flex justify-center pt-16 pb-12">
<button
+4
View File
@@ -7,6 +7,10 @@ declare module '@fontsource/comic-relief/*';
interface ImportMetaEnv {
/** Hex pubkey of the nostr-push server for Web Push notifications. */
readonly VITE_NOSTR_PUSH_PUBKEY?: string;
/** Semver version from package.json (e.g., "2.0.0"). */
readonly VERSION: string;
/** ISO 8601 timestamp of when the app was built (e.g., "2026-03-26T19:42:00.000Z"). */
readonly BUILD_DATE: string;
}
/**
+5 -12
View File
@@ -1,5 +1,5 @@
import process from "node:process";
import { execSync } from "node:child_process";
import { createRequire } from "node:module";
import fs from "node:fs";
import path from "node:path";
@@ -94,16 +94,8 @@ function mergePublicDir(externalDir: string): Plugin {
const dittoConfig = loadDittoConfig();
const publicDir = process.env.PUBLIC_DIR;
/** Git-based version string for Sentry releases. */
function getVersion(): string {
try {
return execSync("git describe --tags --always --dirty", { encoding: "utf-8" }).trim();
} catch {
return "unknown";
}
}
const require = createRequire(import.meta.url);
const pkg = require("./package.json") as { version: string };
// https://vitejs.dev/config/
export default defineConfig(() => {
@@ -118,7 +110,8 @@ export default defineConfig(() => {
],
define: {
__DITTO_CONFIG__: JSON.stringify(dittoConfig ?? null),
'import.meta.env.VERSION': JSON.stringify(getVersion()),
'import.meta.env.VERSION': JSON.stringify(pkg.version),
'import.meta.env.BUILD_DATE': JSON.stringify(new Date().toISOString()),
},
test: {
globals: true,