475 lines
12 KiB
Markdown
475 lines
12 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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:**
|
|
```javascript
|
|
#!/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/MyApp/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
|
|
|
|
```bash
|
|
chmod +x scripts/version-bump.js
|
|
```
|
|
|
|
### Usage
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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 feature
|
|
- `fix`: Bug fix
|
|
- `docs`: Documentation changes
|
|
- `style`: Code style changes
|
|
- `refactor`: Code refactoring
|
|
- `test`: Adding tests
|
|
- `chore`: Maintenance tasks
|
|
|
|
**Examples:**
|
|
```
|
|
feat(auth): add biometric authentication
|
|
fix(api): resolve login timeout issue
|
|
docs(readme): update installation instructions
|
|
```
|
|
|
|
### Automated Changelog
|
|
|
|
**Install standard-version:**
|
|
```bash
|
|
npm install -D standard-version
|
|
```
|
|
|
|
**package.json scripts:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```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/myapp/commit/{{hash}}",
|
|
"compareUrlFormat": "https://github.com/your-org/myapp/compare/{{previousTag}}...{{currentTag}}"
|
|
}
|
|
```
|
|
|
|
## Release Branches
|
|
|
|
### Git Flow Strategy
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```javascript
|
|
// 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:**
|
|
```javascript
|
|
#!/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/MyApp/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:**
|
|
```yaml
|
|
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:**
|
|
```javascript
|
|
#!/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');
|
|
``` |