mirror of
https://github.com/selfxyz/self.git
synced 2026-04-05 03:00:53 -04:00
* feat: add version management system with build number tracking - Add version.json to track iOS/Android build numbers separately - Create version.cjs script for build number management - Add Fastlane version_manager.rb helper - Keep npm version for semver, version.json for build tracking * feat: integrate version.json with Fastlane deployment process ## What Changed - Updated iOS and Android Fastlane lanes to use version.json for build number management - Added automatic build number increment on deployment - Added deployment timestamp tracking ## How It Works ### iOS Deployment 1. Reads current build number from version.json 2. Increments iOS build number (e.g., 148 → 149) 3. Updates Xcode project with new build number via increment_build_number 4. Proceeds with TestFlight deployment 5. Updates lastDeployed timestamp on successful upload ### Android Deployment 1. Reads current build number from version.json 2. Increments Android build number (e.g., 82 → 83) 3. Updates build.gradle with new version code via increment_version_code 4. Proceeds with Play Store deployment 5. Updates lastDeployed timestamp on successful upload ## Why This Change - Eliminates manual version/build number entry - Prevents version conflicts between deployments - Provides single source of truth for build numbers - Enables automatic deployments without human intervention - Tracks deployment history with timestamps ## Dependencies - Requires version.json file (already created in previous commit) - Uses existing Fastlane plugins: - increment_build_number (iOS - built-in) - increment_version_code (Android - from plugin) - Version numbers still managed by npm version command * feat: enhance deploy confirmation with version.json info * fix: use ENV variable directly in increment_build_number to avoid secret masking * fix: correct xcodeproj path for GitHub Actions workflow * feat: add test mode to workflow for safe testing - Skip store uploads when test_mode is true - Test version bumps and builds without deployment - Prevent accidental pushes to TestFlight/Play Store * fix: use gradle_file_path instead of gradle_file for increment_version_code * fix: use gsub to remove ../ prefix for CI compatibility * chore: remove accidentally committed files - Remove .cursor/mcp.json - Remove .cursorignore - Remove deployment-automation-summary.md - Remove deployment-meeting-questions.md - Remove pipeline.md * feat: auto-commit version.json after successful deployment - Commits version.json changes back to repository - Only runs when test_mode is false - Uses [skip ci] to prevent infinite loops - Checks for actual changes before committing * feat : update package.json in build step using npm version * feat: add comprehensive caching to mobile deployment workflow - Add caching for Yarn dependencies, Ruby gems, CocoaPods, Gradle, and Android NDK - Implement cache versioning strategy for easy cache invalidation - Fix cache order: caches now restored after checkout but before dependency installation - Update mobile-setup action to skip installs when dependencies are cached - Add cache size monitoring to track usage against GitHub's 10GB limit - Fix Slack notification bug: skip notifications in test_mode - Add detailed logging for package.json version updates (show from/to versions) Expected performance improvement: ~50% faster builds (from ~15min to ~7-10min) * fix: move bundler config after Ruby setup in mobile-setup action * fix: rename cache env vars to avoid Yarn conflicts Yarn was interpreting YARN_CACHE_VERSION as its own config setting. Prefixed all cache version env vars with GH_ to avoid conflicts. * fix: remove bundler deployment mode to allow Gemfile updates The deployment mode was causing bundler to fail when Gemfile changed (nokogiri was removed). CI should be able to update the lockfile as needed. * feat: implement strict lock file enforcement (Option 1) - Re-enable bundler deployment mode for strict Gemfile.lock checking - Use yarn install --immutable for strict yarn.lock checking - Add clear error messages when lock files are out of date - Add pre-checks to verify lock files exist - This ensures reproducible builds and makes caching maximally effective When developers change dependencies, they must now: 1. Run yarn install or bundle install locally 2. Commit the updated lock files 3. CI will fail with helpful instructions if they forget * fix: update Gemfile.lock for CI environment Remove nokogiri from Gemfile.lock since it's excluded in CI environments (GITHUB_ACTIONS=true). This allows the strict lock file checks to pass in CI. * fix: correct yarn.lock path for monorepo workspace The project uses Yarn workspaces with yarn.lock at the repository root, not in the app directory. Updated paths to check for yarn.lock at workspace root and use it for cache keys. * fix: handle both boolean and string test_mode parameter The test_mode parameter was only checking for string 'true' but could be passed as boolean true from command line. Now handles both cases to ensure test mode works correctly for iOS and Android. * fix: address code review feedback for mobile deployment workflow - Replace jq with Node.js for version extraction (jq not available on macOS runners) - Fix concurrent commit race condition by creating separate update-version job - Add platform validation to version_manager.rb and version.cjs scripts - Use POSIX-compatible single = for shell string comparisons - Ensure single atomic commit when deploying to both platforms * fix: formatting and linting issues - Remove trailing spaces from workflow YAML file - Fix prettier formatting in JavaScript files - Add -y flag to yarn version command for non-interactive mode - Address all lint warnings from CI --------- Co-authored-by: Jayaditya Gupta <nightmare@Jayadityas-MacBook-Pro.local>
682 lines
20 KiB
JavaScript
Executable File
682 lines
20 KiB
JavaScript
Executable File
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
let { execSync } = require('child_process');
|
|
|
|
// Constants
|
|
const DEPLOYMENT_METHODS = {
|
|
GITHUB_RUNNER: 'github-runner',
|
|
LOCAL_FASTLANE: 'local-fastlane',
|
|
};
|
|
|
|
const PLATFORMS = {
|
|
IOS: 'ios',
|
|
ANDROID: 'android',
|
|
BOTH: 'both',
|
|
};
|
|
|
|
const SUPPORTED_PLATFORMS = Object.values(PLATFORMS);
|
|
|
|
const FILE_PATHS = {
|
|
PACKAGE_JSON: '../package.json',
|
|
VERSION_JSON: '../version.json',
|
|
IOS_INFO_PLIST: '../ios/OpenPassport/Info.plist',
|
|
IOS_PROJECT_PBXPROJ: '../ios/Self.xcodeproj/project.pbxproj',
|
|
ANDROID_BUILD_GRADLE: '../android/app/build.gradle',
|
|
};
|
|
|
|
const CONSOLE_SYMBOLS = {
|
|
MOBILE: '📱',
|
|
PACKAGE: '📦',
|
|
ROCKET: '🚀',
|
|
WARNING: '⚠️',
|
|
SUCCESS: '✅',
|
|
ERROR: '❌',
|
|
APPLE: '🍎',
|
|
ANDROID: '🤖',
|
|
CLOUD: '☁️',
|
|
LOCATION: '📍',
|
|
MEMO: '📝',
|
|
CHART: '📊',
|
|
BROOM: '🧹',
|
|
REPEAT: '🔄',
|
|
};
|
|
|
|
const REGEX_PATTERNS = {
|
|
IOS_VERSION:
|
|
/<key>CFBundleShortVersionString<\/key>\s*<string>(.*?)<\/string>/,
|
|
IOS_BUILD: /CURRENT_PROJECT_VERSION = (\d+);/,
|
|
ANDROID_VERSION: /versionName\s+"(.+?)"/,
|
|
ANDROID_VERSION_CODE: /versionCode\s+(\d+)/,
|
|
};
|
|
|
|
// Utility Functions
|
|
|
|
/**
|
|
* Safely reads a file and returns its content or null if failed
|
|
* @param {string} filePath - Path to the file to read
|
|
* @param {string} description - Description of the file for error messages
|
|
* @returns {string|null} File content or null if failed
|
|
*/
|
|
function safeReadFile(filePath, description) {
|
|
try {
|
|
return fs.readFileSync(filePath, 'utf8');
|
|
} catch (error) {
|
|
console.warn(`Warning: Could not read ${description} at ${filePath}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely executes a command and returns its output
|
|
* @param {string} command - Command to execute
|
|
* @param {string} description - Description for error messages
|
|
* @returns {string|null} Command output or null if failed
|
|
*/
|
|
function safeExecSync(command, description) {
|
|
// Whitelist of allowed commands to prevent command injection
|
|
const allowedCommands = [
|
|
'git branch --show-current',
|
|
'git status --porcelain',
|
|
];
|
|
|
|
// Validate that the command is in the whitelist
|
|
if (!allowedCommands.includes(command)) {
|
|
console.warn(
|
|
`Warning: Command '${command}' is not allowed for security reasons`,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return execSync(command, { encoding: 'utf8' }).trim();
|
|
} catch (error) {
|
|
console.warn(`Warning: Could not ${description}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates the provided platform argument
|
|
* @param {string} platform - Platform argument to validate
|
|
* @returns {boolean} True if valid, false otherwise
|
|
*/
|
|
function validatePlatform(platform) {
|
|
return platform && SUPPORTED_PLATFORMS.includes(platform);
|
|
}
|
|
|
|
/**
|
|
* Displays usage information and exits
|
|
*/
|
|
function displayUsageAndExit() {
|
|
console.error('Usage: node mobile-deploy-confirm.cjs <ios|android|both>');
|
|
console.error('');
|
|
console.error('Recommended: Use yarn commands instead:');
|
|
console.error(
|
|
' yarn mobile-deploy # Deploy to both platforms (GitHub runner)',
|
|
);
|
|
console.error(
|
|
' yarn mobile-deploy:ios # Deploy to iOS only (GitHub runner)',
|
|
);
|
|
console.error(
|
|
' yarn mobile-deploy:android # Deploy to Android only (GitHub runner)',
|
|
);
|
|
console.error(
|
|
' yarn mobile-local-deploy # Deploy to both platforms (local fastlane)',
|
|
);
|
|
console.error(
|
|
' yarn mobile-local-deploy:ios # Deploy to iOS only (local fastlane)',
|
|
);
|
|
console.error(
|
|
' yarn mobile-local-deploy:android # Deploy to Android only (local fastlane)',
|
|
);
|
|
console.error('');
|
|
console.error('Direct script usage:');
|
|
console.error(' node mobile-deploy-confirm.cjs ios');
|
|
console.error(' node mobile-deploy-confirm.cjs android');
|
|
console.error(' node mobile-deploy-confirm.cjs both');
|
|
console.error('');
|
|
console.error('Environment Variables:');
|
|
console.error(
|
|
' FORCE_UPLOAD_LOCAL_DEV=true Use local fastlane instead of GitHub runner',
|
|
);
|
|
console.error(
|
|
' IOS_PROJECT_PBXPROJ_PATH Override iOS project.pbxproj path',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Core Functions
|
|
|
|
/**
|
|
* Determines the deployment method based on environment variables
|
|
* @returns {'github-runner' | 'local-fastlane'} The deployment method to use
|
|
*/
|
|
function getDeploymentMethod() {
|
|
// Check if running in GitHub Actions
|
|
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
return DEPLOYMENT_METHODS.GITHUB_RUNNER;
|
|
}
|
|
|
|
// Check if force upload is explicitly set for local development
|
|
if (process.env.FORCE_UPLOAD_LOCAL_DEV === 'true') {
|
|
return DEPLOYMENT_METHODS.LOCAL_FASTLANE;
|
|
}
|
|
|
|
// Default to GitHub runner (safer default)
|
|
// Users must explicitly set FORCE_UPLOAD_LOCAL_DEV=true to use local fastlane
|
|
return DEPLOYMENT_METHODS.GITHUB_RUNNER;
|
|
}
|
|
|
|
/**
|
|
* Reads the main version from package.json
|
|
* @returns {string} The main version number
|
|
*/
|
|
function getMainVersion() {
|
|
const packageJsonPath = path.join(__dirname, FILE_PATHS.PACKAGE_JSON);
|
|
try {
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
return packageJson.version || 'Unknown';
|
|
} catch (error) {
|
|
console.warn(`Warning: Could not parse package.json: ${error.message}`);
|
|
return 'Unknown';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads iOS version information from Info.plist and project.pbxproj
|
|
* @returns {Object} iOS version information
|
|
*/
|
|
function getIOSVersion() {
|
|
const infoPlistPath = path.join(__dirname, FILE_PATHS.IOS_INFO_PLIST);
|
|
const infoPlist = safeReadFile(infoPlistPath, 'iOS Info.plist');
|
|
|
|
if (!infoPlist) {
|
|
return { version: 'Unknown', build: 'Unknown' };
|
|
}
|
|
|
|
const iosVersionMatch = infoPlist.match(REGEX_PATTERNS.IOS_VERSION);
|
|
const version = iosVersionMatch ? iosVersionMatch[1] : 'Unknown';
|
|
|
|
// Extract build number from project.pbxproj
|
|
// Allow iOS project path to be overridden by environment variable
|
|
const iosProjectPath =
|
|
process.env.IOS_PROJECT_PBXPROJ_PATH || FILE_PATHS.IOS_PROJECT_PBXPROJ;
|
|
const projectPath = path.join(__dirname, iosProjectPath);
|
|
const projectFile = safeReadFile(projectPath, 'iOS project.pbxproj');
|
|
|
|
let build = 'Unknown';
|
|
if (projectFile) {
|
|
const buildMatch = projectFile.match(REGEX_PATTERNS.IOS_BUILD);
|
|
build = buildMatch ? buildMatch[1] : 'Unknown';
|
|
}
|
|
|
|
return { version, build };
|
|
}
|
|
|
|
/**
|
|
* Reads Android version information from build.gradle
|
|
* @returns {Object} Android version information
|
|
*/
|
|
function getAndroidVersion() {
|
|
const buildGradlePath = path.join(__dirname, FILE_PATHS.ANDROID_BUILD_GRADLE);
|
|
const buildGradle = safeReadFile(buildGradlePath, 'Android build.gradle');
|
|
|
|
if (!buildGradle) {
|
|
return { version: 'Unknown', versionCode: 'Unknown' };
|
|
}
|
|
|
|
const androidVersionMatch = buildGradle.match(REGEX_PATTERNS.ANDROID_VERSION);
|
|
const androidVersionCodeMatch = buildGradle.match(
|
|
REGEX_PATTERNS.ANDROID_VERSION_CODE,
|
|
);
|
|
|
|
return {
|
|
version: androidVersionMatch ? androidVersionMatch[1] : 'Unknown',
|
|
versionCode: androidVersionCodeMatch
|
|
? androidVersionCodeMatch[1]
|
|
: 'Unknown',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reads version.json for build numbers and deployment history
|
|
* @returns {Object|null} Version data or null if not found
|
|
*/
|
|
function getVersionJsonData() {
|
|
const versionJsonPath = path.join(__dirname, FILE_PATHS.VERSION_JSON);
|
|
try {
|
|
const versionData = JSON.parse(fs.readFileSync(versionJsonPath, 'utf8'));
|
|
return versionData;
|
|
} catch (error) {
|
|
console.warn(`Warning: Could not read version.json: ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats time elapsed since last deployment
|
|
* @param {string} timestamp - ISO timestamp of last deployment
|
|
* @returns {string} Human-readable time elapsed
|
|
*/
|
|
function getTimeAgo(timestamp) {
|
|
if (!timestamp) return 'Never deployed';
|
|
|
|
const now = new Date();
|
|
const then = new Date(timestamp);
|
|
const diffMs = now - then;
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffDays > 0) {
|
|
return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
|
|
} else if (diffHours > 0) {
|
|
return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
|
} else {
|
|
return 'Less than an hour ago';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads version information from package.json, iOS Info.plist, and Android build.gradle
|
|
* @returns {Object} Object containing version information for all platforms
|
|
*/
|
|
function getCurrentVersions() {
|
|
const versionJson = getVersionJsonData();
|
|
|
|
return {
|
|
main: getMainVersion(),
|
|
ios: getIOSVersion(),
|
|
android: getAndroidVersion(),
|
|
versionJson: versionJson,
|
|
};
|
|
}
|
|
|
|
// Git Operations
|
|
|
|
/**
|
|
* Gets the current git branch name
|
|
* @returns {string|null} Current branch name or null if failed
|
|
*/
|
|
function getCurrentBranch() {
|
|
return safeExecSync(
|
|
'git branch --show-current',
|
|
'determine current git branch',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Checks if there are uncommitted changes
|
|
* @returns {boolean} True if there are uncommitted changes
|
|
*/
|
|
function hasUncommittedChanges() {
|
|
const gitStatus = safeExecSync('git status --porcelain', 'check git status');
|
|
return gitStatus && gitStatus.trim().length > 0;
|
|
}
|
|
|
|
// Display Functions
|
|
|
|
/**
|
|
* Displays the header and platform information
|
|
* @param {string} platform - Target platform
|
|
*/
|
|
function displayDeploymentHeader(platform) {
|
|
console.log(`\n${CONSOLE_SYMBOLS.MOBILE} Mobile App Deployment Confirmation`);
|
|
console.log('=====================================');
|
|
console.log(`${CONSOLE_SYMBOLS.ROCKET} Platform: ${platform.toUpperCase()}`);
|
|
}
|
|
|
|
/**
|
|
* Displays deployment method information
|
|
* @param {string} deploymentMethod - The deployment method to use
|
|
*/
|
|
function displayDeploymentMethod(deploymentMethod) {
|
|
if (deploymentMethod === DEPLOYMENT_METHODS.LOCAL_FASTLANE) {
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.LOCATION} Deployment: Local fastlane upload`,
|
|
);
|
|
} else {
|
|
console.log(`${CONSOLE_SYMBOLS.CLOUD} Deployment: GitHub Actions workflow`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays platform-specific version information
|
|
* @param {string} platform - Target platform
|
|
* @param {Object} versions - Version information object
|
|
*/
|
|
function displayPlatformVersions(platform, versions) {
|
|
console.log(`${CONSOLE_SYMBOLS.PACKAGE} Main Version: ${versions.main}`);
|
|
|
|
if (platform === PLATFORMS.IOS || platform === PLATFORMS.BOTH) {
|
|
const currentBuild = versions.ios.build;
|
|
const nextBuild = versions.versionJson
|
|
? versions.versionJson.ios.build + 1
|
|
: parseInt(currentBuild) + 1;
|
|
const lastDeployed = versions.versionJson
|
|
? getTimeAgo(versions.versionJson.ios.lastDeployed)
|
|
: 'Unknown';
|
|
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.APPLE} iOS Version: ${versions.ios.version}`,
|
|
);
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.APPLE} iOS Build: ${currentBuild} → ${nextBuild}`,
|
|
);
|
|
console.log(`${CONSOLE_SYMBOLS.APPLE} Last iOS Deploy: ${lastDeployed}`);
|
|
}
|
|
|
|
if (platform === PLATFORMS.ANDROID || platform === PLATFORMS.BOTH) {
|
|
const currentBuild = versions.android.versionCode;
|
|
const nextBuild = versions.versionJson
|
|
? versions.versionJson.android.build + 1
|
|
: parseInt(currentBuild) + 1;
|
|
const lastDeployed = versions.versionJson
|
|
? getTimeAgo(versions.versionJson.android.lastDeployed)
|
|
: 'Unknown';
|
|
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.ANDROID} Android Version: ${versions.android.version}`,
|
|
);
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.ANDROID} Android Version Code: ${currentBuild} → ${nextBuild}`,
|
|
);
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.ANDROID} Last Android Deploy: ${lastDeployed}`,
|
|
);
|
|
}
|
|
|
|
// Check for potential issues
|
|
if (versions.versionJson) {
|
|
if (platform === PLATFORMS.IOS || platform === PLATFORMS.BOTH) {
|
|
const jsonBuild = versions.versionJson.ios.build;
|
|
const actualBuild = parseInt(versions.ios.build);
|
|
if (jsonBuild !== actualBuild) {
|
|
console.log(
|
|
`\n${CONSOLE_SYMBOLS.WARNING} iOS build mismatch: version.json has ${jsonBuild}, but Xcode has ${actualBuild}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (platform === PLATFORMS.ANDROID || platform === PLATFORMS.BOTH) {
|
|
const jsonBuild = versions.versionJson.android.build;
|
|
const actualBuild = parseInt(versions.android.versionCode);
|
|
if (jsonBuild !== actualBuild) {
|
|
console.log(
|
|
`\n${CONSOLE_SYMBOLS.WARNING} Android build mismatch: version.json has ${jsonBuild}, but gradle has ${actualBuild}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays warnings and git status information
|
|
*/
|
|
function displayWarningsAndGitStatus() {
|
|
const currentBranch = getCurrentBranch();
|
|
const hasUncommitted = hasUncommittedChanges();
|
|
|
|
console.log(`\n${CONSOLE_SYMBOLS.WARNING} Important Notes:`);
|
|
console.log(
|
|
'• Deploys to internal testing (TestFlight/Google Play Internal)',
|
|
);
|
|
if (currentBranch) {
|
|
console.log(`• Current branch: ${currentBranch}`);
|
|
}
|
|
if (hasUncommitted) {
|
|
console.log('• You have uncommitted changes - consider committing first');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays all confirmation information
|
|
* @param {string} platform - Target platform
|
|
* @param {Object} versions - Version information object
|
|
* @param {string} deploymentMethod - The deployment method to use
|
|
*/
|
|
function displayFullConfirmation(platform, versions, deploymentMethod) {
|
|
displayDeploymentHeader(platform);
|
|
displayDeploymentMethod(deploymentMethod);
|
|
displayPlatformVersions(platform, versions);
|
|
displayWarningsAndGitStatus();
|
|
}
|
|
|
|
/**
|
|
* Prompts the user for confirmation
|
|
* @returns {Promise<boolean>} True if user confirms, false otherwise
|
|
*/
|
|
function promptConfirmation() {
|
|
const readline = require('readline').createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
});
|
|
|
|
return new Promise(resolve => {
|
|
readline.question('\nDo you want to proceed? (y/N): ', answer => {
|
|
readline.close();
|
|
// Trim whitespace and normalize to lowercase for robust comparison
|
|
const normalizedAnswer = answer.trim().toLowerCase();
|
|
resolve(normalizedAnswer === 'y' || normalizedAnswer === 'yes');
|
|
});
|
|
});
|
|
}
|
|
|
|
// Deployment Functions
|
|
|
|
/**
|
|
* Performs yarn reinstall to ensure clean dependencies
|
|
*/
|
|
function performYarnReinstall() {
|
|
console.log(
|
|
`\n${CONSOLE_SYMBOLS.BROOM} Performing yarn reinstall to ensure clean dependencies...`,
|
|
);
|
|
execSync('yarn reinstall', {
|
|
stdio: 'inherit',
|
|
cwd: path.join(__dirname, '..'),
|
|
});
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.SUCCESS} Yarn reinstall completed successfully!`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Gets the fastlane commands for the specified platform
|
|
* @param {string} platform - Target platform
|
|
* @returns {string[]} Array of fastlane commands to execute
|
|
*/
|
|
function getFastlaneCommands(platform) {
|
|
const commands = [];
|
|
|
|
if (platform === PLATFORMS.IOS || platform === PLATFORMS.BOTH) {
|
|
commands.push('cd .. && bundle exec fastlane ios internal_test');
|
|
}
|
|
|
|
if (platform === PLATFORMS.ANDROID || platform === PLATFORMS.BOTH) {
|
|
commands.push('cd .. && bundle exec fastlane android internal_test');
|
|
}
|
|
|
|
return commands;
|
|
}
|
|
|
|
/**
|
|
* Executes iOS build cleanup script
|
|
* @param {string} platform - Target platform
|
|
*/
|
|
let performIOSBuildCleanup = function (platform) {
|
|
// Only run cleanup for iOS deployments
|
|
if (platform !== PLATFORMS.IOS && platform !== PLATFORMS.BOTH) {
|
|
return;
|
|
}
|
|
|
|
console.log(`\n${CONSOLE_SYMBOLS.BROOM} Cleaning up iOS build artifacts...`);
|
|
|
|
try {
|
|
const cleanupScript = path.join(__dirname, 'cleanup-ios-build.sh');
|
|
execSync(`bash "${cleanupScript}"`, {
|
|
stdio: 'inherit',
|
|
cwd: __dirname,
|
|
});
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.SUCCESS} iOS build cleanup completed successfully!`,
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
`${CONSOLE_SYMBOLS.WARNING} iOS build cleanup failed (non-fatal):`,
|
|
error.message,
|
|
);
|
|
// Don't exit on cleanup failure - it's not critical
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Executes local fastlane deployment
|
|
* @param {string} platform - Target platform
|
|
*/
|
|
async function executeLocalFastlaneDeployment(platform) {
|
|
console.log(
|
|
`\n${CONSOLE_SYMBOLS.ROCKET} Starting local fastlane deployment...`,
|
|
);
|
|
|
|
let deploymentSuccessful = false;
|
|
|
|
try {
|
|
performYarnReinstall();
|
|
|
|
const commands = getFastlaneCommands(platform);
|
|
|
|
// Create environment with FORCE_UPLOAD_LOCAL_DEV set for child processes
|
|
const envWithForceUpload = {
|
|
...process.env,
|
|
FORCE_UPLOAD_LOCAL_DEV: 'true',
|
|
};
|
|
|
|
for (const command of commands) {
|
|
console.log(`\n${CONSOLE_SYMBOLS.REPEAT} Running: ${command}`);
|
|
execSync(command, {
|
|
stdio: 'inherit',
|
|
cwd: __dirname,
|
|
env: envWithForceUpload,
|
|
});
|
|
}
|
|
|
|
deploymentSuccessful = true;
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.SUCCESS} Local fastlane deployment completed successfully!`,
|
|
);
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.MOBILE} Check your app store dashboards for the new builds.`,
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
`${CONSOLE_SYMBOLS.ERROR} Local fastlane deployment failed:`,
|
|
error.message,
|
|
);
|
|
} finally {
|
|
// Always run cleanup after deployment, regardless of success/failure
|
|
performIOSBuildCleanup(platform);
|
|
|
|
// Only exit with error code if deployment failed
|
|
if (!deploymentSuccessful) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes GitHub runner deployment
|
|
* @param {string} platform - Target platform
|
|
*/
|
|
async function executeGithubRunnerDeployment(platform) {
|
|
console.log(
|
|
`\n${CONSOLE_SYMBOLS.ROCKET} Starting GitHub runner deployment...`,
|
|
);
|
|
|
|
// Safely get the current branch name to avoid command injection
|
|
const currentBranch = getCurrentBranch();
|
|
if (!currentBranch) {
|
|
console.error(
|
|
`${CONSOLE_SYMBOLS.ERROR} Could not determine current git branch`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const command = `gh workflow run mobile-deploy.yml --ref ${currentBranch} -f platform=${platform}`;
|
|
|
|
try {
|
|
execSync(command, { stdio: 'inherit' });
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.SUCCESS} GitHub workflow triggered successfully!`,
|
|
);
|
|
console.log(
|
|
`${CONSOLE_SYMBOLS.CHART} Check GitHub Actions for build progress.`,
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
`${CONSOLE_SYMBOLS.ERROR} Failed to trigger GitHub workflow:`,
|
|
error.message,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Executes the deployment based on the specified method
|
|
* @param {string} platform - Target platform
|
|
* @param {string} deploymentMethod - The deployment method to use
|
|
*/
|
|
async function executeDeployment(platform, deploymentMethod) {
|
|
if (deploymentMethod === DEPLOYMENT_METHODS.LOCAL_FASTLANE) {
|
|
await executeLocalFastlaneDeployment(platform);
|
|
} else {
|
|
await executeGithubRunnerDeployment(platform);
|
|
}
|
|
}
|
|
|
|
// Main Function
|
|
|
|
/**
|
|
* Main function that orchestrates the deployment confirmation process
|
|
*/
|
|
async function main() {
|
|
const platform = process.argv[2];
|
|
|
|
if (!validatePlatform(platform)) {
|
|
displayUsageAndExit();
|
|
}
|
|
|
|
const deploymentMethod = getDeploymentMethod();
|
|
const versions = getCurrentVersions();
|
|
|
|
displayFullConfirmation(platform, versions, deploymentMethod);
|
|
|
|
const confirmed = await promptConfirmation();
|
|
|
|
if (confirmed) {
|
|
await executeDeployment(platform, deploymentMethod);
|
|
} else {
|
|
console.log(`\n${CONSOLE_SYMBOLS.ERROR} Deployment cancelled.`);
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
// Execute main function
|
|
if (require.main === module) {
|
|
main().catch(error => {
|
|
console.error(`${CONSOLE_SYMBOLS.ERROR} Error:`, error.message);
|
|
process.exit(1);
|
|
});
|
|
} else {
|
|
module.exports = {
|
|
performIOSBuildCleanup,
|
|
executeLocalFastlaneDeployment,
|
|
_setExecSync: fn => {
|
|
execSync = fn;
|
|
},
|
|
_setPerformIOSBuildCleanup: fn => {
|
|
performIOSBuildCleanup = fn;
|
|
},
|
|
};
|
|
}
|