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

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 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:

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