12 KiB
12 KiB
Version Management
Semantic Versioning
Version Format
MAJOR.MINOR.PATCH
Examples:
1.0.0 - Initial release
1.0.1 - Bug fix
1.1.0 - New feature
2.0.0 - Breaking changes
Version Rules
- MAJOR: Breaking changes, incompatible API changes
- MINOR: New features, backward compatible
- PATCH: Bug fixes, backward compatible
Automated Versioning
Package.json Version Management
# Install version management tools
npm install -g standard-version
# Bump version automatically
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.0 -> 1.1.0
npm version major # 1.0.0 -> 2.0.0
# With standard-version (recommended)
npx standard-version --release-as patch
npx standard-version --release-as minor
npx standard-version --release-as major
Cross-Platform Version Script
scripts/version-bump.js:
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const versionType = process.argv[2] || 'patch';
const packageJsonPath = path.join(__dirname, '../package.json');
const androidBuildGradle = path.join(__dirname, '../android/app/build.gradle');
const iosPlistPath = path.join(__dirname, '../ios/SaayamApp/Info.plist');
// Read current version from package.json
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
console.log(`Current version: ${currentVersion}`);
// Bump version in package.json
execSync(`npm version ${versionType} --no-git-tag-version`, { stdio: 'inherit' });
// Read new version
const updatedPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const newVersion = updatedPackageJson.version;
console.log(`New version: ${newVersion}`);
// Update Android version
updateAndroidVersion(newVersion);
// Update iOS version
updateiOSVersion(newVersion);
// Create git commit and tag
execSync(`git add .`, { stdio: 'inherit' });
execSync(`git commit -m "chore: bump version to ${newVersion}"`, { stdio: 'inherit' });
execSync(`git tag -a v${newVersion} -m "Release version ${newVersion}"`, { stdio: 'inherit' });
console.log(`Version bumped to ${newVersion} successfully!`);
function updateAndroidVersion(version) {
let buildGradleContent = fs.readFileSync(androidBuildGradle, 'utf8');
// Update versionName
buildGradleContent = buildGradleContent.replace(
/versionName\s+"[^"]*"/,
`versionName "${version}"`
);
// Update versionCode (increment by 1)
const versionCodeMatch = buildGradleContent.match(/versionCode\s+(\d+)/);
if (versionCodeMatch) {
const currentVersionCode = parseInt(versionCodeMatch[1]);
const newVersionCode = currentVersionCode + 1;
buildGradleContent = buildGradleContent.replace(
/versionCode\s+\d+/,
`versionCode ${newVersionCode}`
);
console.log(`Android versionCode updated to: ${newVersionCode}`);
}
fs.writeFileSync(androidBuildGradle, buildGradleContent);
console.log(`Android versionName updated to: ${version}`);
}
function updateiOSVersion(version) {
try {
// Update CFBundleShortVersionString
execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${version}" "${iosPlistPath}"`, { stdio: 'inherit' });
// Get current build number and increment
const currentBuild = execSync(`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${iosPlistPath}"`, { encoding: 'utf8' }).trim();
const newBuild = parseInt(currentBuild) + 1;
// Update CFBundleVersion
execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${newBuild}" "${iosPlistPath}"`, { stdio: 'inherit' });
console.log(`iOS version updated to: ${version} (${newBuild})`);
} catch (error) {
console.error('Error updating iOS version:', error.message);
}
}
Make Script Executable
chmod +x scripts/version-bump.js
Usage
# Bump patch version
./scripts/version-bump.js patch
# Bump minor version
./scripts/version-bump.js minor
# Bump major version
./scripts/version-bump.js major
Changelog Management
Conventional Commits
# Install commitizen for conventional commits
npm install -g commitizen cz-conventional-changelog
# Configure commitizen
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc
# Use commitizen for commits
git cz
Commit Message Format
<type>(<scope>): <subject>
<body>
<footer>
Types:
feat
: New featurefix
: Bug fixdocs
: Documentation changesstyle
: Code style changesrefactor
: Code refactoringtest
: Adding testschore
: Maintenance tasks
Examples:
feat(auth): add biometric authentication
fix(api): resolve login timeout issue
docs(readme): update installation instructions
Automated Changelog
Install standard-version:
npm install -D standard-version
package.json scripts:
{
"scripts": {
"release": "standard-version",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major",
"release:patch": "standard-version --release-as patch"
}
}
.versionrc.json:
{
"types": [
{"type": "feat", "section": "Features"},
{"type": "fix", "section": "Bug Fixes"},
{"type": "chore", "hidden": true},
{"type": "docs", "hidden": true},
{"type": "style", "hidden": true},
{"type": "refactor", "section": "Code Refactoring"},
{"type": "perf", "section": "Performance Improvements"},
{"type": "test", "hidden": true}
],
"commitUrlFormat": "https://github.com/your-org/saayam-app/commit/{{hash}}",
"compareUrlFormat": "https://github.com/your-org/saayam-app/compare/{{previousTag}}...{{currentTag}}"
}
Release Branches
Git Flow Strategy
# Install git-flow
brew install git-flow-avh # macOS
apt-get install git-flow # Ubuntu
# Initialize git-flow
git flow init
# Start a release branch
git flow release start 1.2.0
# Finish a release branch
git flow release finish 1.2.0
Branch Structure
main (production)
├── develop (development)
├── feature/user-authentication
├── feature/payment-integration
├── release/1.2.0
├── hotfix/critical-bug-fix
Release Process
# 1. Create release branch from develop
git checkout develop
git pull origin develop
git checkout -b release/1.2.0
# 2. Update version numbers
./scripts/version-bump.js minor
# 3. Run final tests
npm test
npm run e2e
# 4. Merge to main
git checkout main
git merge release/1.2.0
# 5. Tag release
git tag -a v1.2.0 -m "Release version 1.2.0"
# 6. Merge back to develop
git checkout develop
git merge release/1.2.0
# 7. Push everything
git push origin main
git push origin develop
git push origin v1.2.0
# 8. Delete release branch
git branch -d release/1.2.0
Build Numbers
Platform-Specific Build Numbers
Android (versionCode):
- Integer that increases with each release
- Used by Google Play to determine newer versions
- Must be incremented for each upload
iOS (CFBundleVersion):
- String that increases with each build
- Used by App Store to determine newer builds
- Can be numeric or alphanumeric
Build Number Strategy
// Generate build number based on timestamp
function generateBuildNumber() {
const now = new Date();
const year = now.getFullYear().toString().slice(-2);
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hour = now.getHours().toString().padStart(2, '0');
const minute = now.getMinutes().toString().padStart(2, '0');
return `${year}${month}${day}${hour}${minute}`;
}
// Example: 2312151430 (23-12-15 14:30)
Version Validation
Pre-Release Checks
scripts/validate-version.js:
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const packageJsonPath = path.join(__dirname, '../package.json');
const androidBuildGradle = path.join(__dirname, '../android/app/build.gradle');
const iosPlistPath = path.join(__dirname, '../ios/SaayamApp/Info.plist');
function validateVersions() {
// Get package.json version
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const packageVersion = packageJson.version;
// Get Android version
const buildGradleContent = fs.readFileSync(androidBuildGradle, 'utf8');
const androidVersionMatch = buildGradleContent.match(/versionName\s+"([^"]*)"/);
const androidVersion = androidVersionMatch ? androidVersionMatch[1] : null;
// Get iOS version
let iosVersion = null;
try {
const { execSync } = require('child_process');
iosVersion = execSync(`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${iosPlistPath}"`, { encoding: 'utf8' }).trim();
} catch (error) {
console.error('Could not read iOS version');
}
console.log('Version Validation:');
console.log(`Package.json: ${packageVersion}`);
console.log(`Android: ${androidVersion}`);
console.log(`iOS: ${iosVersion}`);
const allVersionsMatch = packageVersion === androidVersion && packageVersion === iosVersion;
if (allVersionsMatch) {
console.log('✅ All versions match!');
process.exit(0);
} else {
console.log('❌ Version mismatch detected!');
process.exit(1);
}
}
validateVersions();
CI/CD Integration
.github/workflows/version-check.yml:
name: Version Check
on:
pull_request:
branches: [main]
jobs:
version-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Validate versions
run: node scripts/validate-version.js
Release Notes
Automated Release Notes
scripts/generate-release-notes.js:
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
function generateReleaseNotes(fromTag, toTag) {
// Get commits between tags
const commits = execSync(`git log ${fromTag}..${toTag} --pretty=format:"%h %s"`, { encoding: 'utf8' })
.split('\n')
.filter(line => line.trim());
const features = [];
const fixes = [];
const others = [];
commits.forEach(commit => {
if (commit.includes('feat:') || commit.includes('feat(')) {
features.push(commit.replace(/^\w+\s+/, '').replace(/^feat(\([^)]+\))?:\s*/, ''));
} else if (commit.includes('fix:') || commit.includes('fix(')) {
fixes.push(commit.replace(/^\w+\s+/, '').replace(/^fix(\([^)]+\))?:\s*/, ''));
} else {
others.push(commit.replace(/^\w+\s+/, ''));
}
});
let releaseNotes = `# Release Notes\n\n`;
if (features.length > 0) {
releaseNotes += `## 🚀 New Features\n`;
features.forEach(feature => {
releaseNotes += `- ${feature}\n`;
});
releaseNotes += '\n';
}
if (fixes.length > 0) {
releaseNotes += `## 🐛 Bug Fixes\n`;
fixes.forEach(fix => {
releaseNotes += `- ${fix}\n`;
});
releaseNotes += '\n';
}
if (others.length > 0) {
releaseNotes += `## 🔧 Other Changes\n`;
others.forEach(other => {
releaseNotes += `- ${other}\n`;
});
}
return releaseNotes;
}
// Usage: node generate-release-notes.js v1.0.0 v1.1.0
const fromTag = process.argv[2];
const toTag = process.argv[3] || 'HEAD';
if (!fromTag) {
console.error('Usage: node generate-release-notes.js <from-tag> [to-tag]');
process.exit(1);
}
const releaseNotes = generateReleaseNotes(fromTag, toTag);
console.log(releaseNotes);
// Save to file
fs.writeFileSync('RELEASE_NOTES.md', releaseNotes);
console.log('\nRelease notes saved to RELEASE_NOTES.md');