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:
@@ -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
@@ -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:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [2.0.0] - 2026-03-26
|
||||
|
||||
Initial release of Ditto 2.0 -- a complete rewrite of Ditto.
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+4
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user