react-native-sop-docs/10-release-process/versioning.md

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');
```