code rabbit feedback for staging auto deploy logic (#1255)

* code rabbit feedback for staging auto deploy logic

* fix jest test

* cr feedback

* workflow fixes

* fix tests

* restore cache
This commit is contained in:
Justin Hernandez
2025-10-17 07:58:02 -07:00
committed by GitHub
parent 16b1b1cde0
commit e01ec188af
7 changed files with 430 additions and 28 deletions

View File

@@ -56,9 +56,7 @@ env:
IOS_PROV_PROFILE_DIRECTORY: "~/Library/MobileDevice/Provisioning\ Profiles/"
permissions:
contents: write
pull-requests: write
id-token: write
contents: read
on:
workflow_dispatch:
@@ -150,6 +148,8 @@ jobs:
# NOTE: Checks out the triggering branch (staging for PR merges, or the branch where manually triggered)
bump-version:
runs-on: ubuntu-latest
permissions:
contents: read
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
!contains(github.event.pull_request.labels.*.name, 'deploy:skip')
@@ -241,9 +241,26 @@ jobs:
echo "✅ Version bump calculated successfully"
echo "⚠️ Note: Changes are local only. Will be committed in PR after successful builds."
- name: Verify bump outputs were set
run: |
VERSION="${{ steps.bump.outputs.version }}"
IOS_BUILD="${{ steps.bump.outputs.ios_build }}"
ANDROID_BUILD="${{ steps.bump.outputs.android_build }}"
if [ -z "$VERSION" ] || [ -z "$IOS_BUILD" ] || [ -z "$ANDROID_BUILD" ]; then
echo "❌ Version bump failed to set required outputs"
echo "version='$VERSION', ios_build='$IOS_BUILD', android_build='$ANDROID_BUILD'"
exit 1
fi
echo "✅ All version outputs verified"
build-ios:
needs: [bump-version]
runs-on: macos-latest-large
permissions:
contents: read
actions: write
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
(
@@ -285,7 +302,7 @@ jobs:
echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV"
- name: Verify branch and commit (iOS)
if: inputs.platform != 'android'
if: needs.bump-version.outputs.platform != 'android'
run: |
echo "🔍 Verifying we're building from the correct branch and commit..."
echo "Current branch: $(git branch --show-current || git symbolic-ref --short HEAD 2>/dev/null || echo 'detached')"
@@ -309,7 +326,7 @@ jobs:
fi
- name: Apply version bump for build
if: inputs.platform != 'android'
if: needs.bump-version.outputs.platform != 'android'
run: |
cd ${{ env.APP_PATH }}
@@ -804,6 +821,10 @@ jobs:
build-android:
needs: [bump-version]
runs-on: ubuntu-latest
permissions:
contents: read
actions: write
id-token: write
if: |
(github.event_name != 'pull_request' || github.event.pull_request.merged == true) &&
(
@@ -910,7 +931,7 @@ jobs:
echo "NODE_VERSION_SANITIZED=${VERSION//\//-}" >> "$GITHUB_ENV"
- name: Verify branch and commit (Android)
if: inputs.platform != 'ios'
if: needs.bump-version.outputs.platform != 'ios'
run: |
echo "🔍 Verifying we're building from the correct branch and commit..."
echo "Current branch: $(git branch --show-current || git symbolic-ref --short HEAD 2>/dev/null || echo 'detached')"
@@ -930,7 +951,7 @@ jobs:
fi
- name: Apply version bump for build
if: inputs.platform != 'ios'
if: needs.bump-version.outputs.platform != 'ios'
run: |
cd ${{ env.APP_PATH }}
@@ -1234,6 +1255,9 @@ jobs:
# but create the version bump PR to dev so it can be reviewed before merging to staging
create-version-bump-pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
needs: [bump-version, build-ios, build-android]
if: |
always() &&
@@ -1284,6 +1308,20 @@ jobs:
echo "✅ Versions applied successfully"
- name: Verify version changes
run: |
cd ${{ env.APP_PATH }}
# Check that version files actually changed
if ! git diff --quiet package.json version.json; then
echo "✅ Version changes detected"
git diff package.json version.json
else
echo "⚠️ No version changes detected in package.json or version.json"
echo "This may indicate a problem with version application"
exit 1
fi
- name: Determine platforms that succeeded and PR title
id: platforms
run: |
@@ -1383,6 +1421,9 @@ jobs:
# Create git tags after successful deployment
create-release-tags:
runs-on: ubuntu-latest
permissions:
contents: write
needs: [bump-version, build-ios, build-android, create-version-bump-pr]
if: |
always() &&
@@ -1390,7 +1431,6 @@ jobs:
needs.create-version-bump-pr.result == 'success' &&
(needs.build-ios.result == 'success' || needs.build-android.result == 'success') &&
(inputs.deployment_track == 'production')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:

View File

@@ -160,7 +160,7 @@ jobs:
- name: Create dev to staging release PR
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.check_dev_staging.outputs.existing_pr == '' }}
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
PR_DATE: ${{ steps.check_dev_staging.outputs.date }}
BRANCH_NAME: ${{ steps.check_dev_staging.outputs.branch_name }}
shell: bash
@@ -328,7 +328,7 @@ jobs:
- name: Create staging to main release PR
if: ${{ steps.guard_schedule.outputs.continue == 'true' && steps.production_status.outputs.staging_not_ahead != 'true' && steps.production_status.outputs.existing_pr == '' }}
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ secrets.SELFXYZ_INTERNAL_REPO_PAT }}
PR_DATE: ${{ steps.production_status.outputs.date }}
COMMITS_AHEAD: ${{ steps.production_status.outputs.commits }}
shell: bash

View File

@@ -49,12 +49,12 @@ ios_xcode_profile_path = "../ios/#{PROJECT_NAME}.xcodeproj"
default_platform(:ios)
platform :ios do
desc "Sync ios version"
desc "Sync ios version (DEPRECATED)"
lane :sync_version do
increment_version_number(
xcodeproj: "ios/#{PROJECT_NAME}.xcodeproj",
version_number: package_version,
)
UI.error("⛔ This lane is deprecated!")
UI.error("Version management is now centralized in CI.")
UI.error("Use: node scripts/version-manager.cjs apply <version> <ios> <android>")
UI.user_error!("sync_version lane is deprecated - use version-manager.cjs instead")
end
desc "Push a new build to TestFlight Internal Testing"
@@ -247,12 +247,12 @@ platform :ios do
end
platform :android do
desc "Sync android version"
desc "Sync android version (DEPRECATED)"
lane :sync_version do
android_set_version_name(
version_name: package_version,
gradle_file: android_gradle_file_path.gsub("../", ""),
)
UI.error("⛔ This lane is deprecated!")
UI.error("Version management is now centralized in CI.")
UI.error("Use: node scripts/version-manager.cjs apply <version> <ios> <android>")
UI.user_error!("sync_version lane is deprecated - use version-manager.cjs instead")
end
desc "Push a new build to Google Play Internal Testing"

View File

@@ -63,10 +63,26 @@ module Fastlane
android_matches = android_build == expected_android_build
unless version_matches && ios_matches && android_matches
UI.error("Version mismatch detected!")
UI.error("Version mismatch detected!")
UI.error("Expected: v#{expected_version} (iOS: #{expected_ios_build}, Android: #{expected_android_build})")
UI.error("Actual: v#{pkg_version} (iOS: #{ios_build}, Android: #{android_build})")
UI.user_error!("Version mismatch! CI version-manager script should have set these correctly.")
UI.error("")
# Add specific diagnostics
UI.error("Mismatched fields:")
UI.error(" • package.json version") unless version_matches
UI.error(" • version.json iOS build") unless ios_matches
UI.error(" • version.json Android build") unless android_matches
UI.error("")
UI.error("💡 Common causes:")
UI.error(" 1. version-manager.cjs 'apply' command didn't run in workflow")
UI.error(" 2. Files were modified after version bump was applied")
UI.error(" 3. CI_VERSION, CI_IOS_BUILD, or CI_ANDROID_BUILD env vars are incorrect")
UI.error("")
UI.error("🔍 Debug: Check workflow logs for 'Apply version bump' step")
UI.user_error!("Version verification failed")
end
UI.success("✅ Version verification passed:")

View File

@@ -4,12 +4,19 @@
module.exports = {
preset: 'react-native',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'cjs', 'json', 'node'],
transformIgnorePatterns: [
'node_modules/(?!(react-native|@react-native|@react-navigation|@react-native-community|@segment/analytics-react-native|@openpassport|react-native-keychain|react-native-check-version|react-native-nfc-manager|react-native-passport-reader|react-native-gesture-handler|uuid|@stablelib|@react-native-google-signin|react-native-cloud-storage|@react-native-clipboard|@react-native-firebase|@selfxyz|@sentry|@anon-aadhaar|react-native-svg|react-native-svg-circle-country-flags)/)',
],
setupFiles: ['<rootDir>/jest.setup.js'],
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.[jt]sx?$',
testMatch: [
'<rootDir>/**/__tests__/**/*.{js,jsx,ts,tsx,cjs}',
'<rootDir>/**/?(*.)+(spec|test).{js,jsx,ts,tsx,cjs}',
],
testPathIgnorePatterns: [
'/node_modules/',
'/scripts/tests/', // Node.js native test runner tests
],
moduleNameMapper: {
'^@env$': '<rootDir>/tests/__setup__/@env.js',
'\\.svg$': '<rootDir>/tests/__setup__/svgMock.js',

View File

@@ -204,10 +204,33 @@ function bumpVersion(bumpType, platform = 'both') {
* Apply version changes to files
*/
function applyVersions(version, iosBuild, androidBuild) {
// Validate version format (semver X.Y.Z)
if (
!version ||
typeof version !== 'string' ||
!/^\d+\.\d+\.\d+$/.test(version)
) {
throw new Error(`Invalid version format: ${version}. Expected X.Y.Z`);
}
// Validate and coerce build numbers
const iosNum = Number(iosBuild);
const androidNum = Number(androidBuild);
if (!Number.isInteger(iosNum) || iosNum < 1) {
throw new Error(`Invalid iOS build: ${iosBuild}. Must be positive integer`);
}
if (!Number.isInteger(androidNum) || androidNum < 1) {
throw new Error(
`Invalid Android build: ${androidBuild}. Must be positive integer`,
);
}
console.log(`📝 Applying versions to files...`);
console.log(` Version: ${version}`);
console.log(` iOS Build: ${iosBuild}`);
console.log(` Android Build: ${androidBuild}`);
console.log(` iOS Build: ${iosNum}`);
console.log(` Android Build: ${androidNum}`);
// Update package.json
const pkg = readPackageJson();
@@ -217,8 +240,8 @@ function applyVersions(version, iosBuild, androidBuild) {
// Update version.json
const versionData = readVersionJson();
versionData.ios.build = iosBuild;
versionData.android.build = androidBuild;
versionData.ios.build = iosNum;
versionData.android.build = androidNum;
writeVersionJson(versionData);
console.log(`✅ Updated version.json`);
}

View File

@@ -0,0 +1,316 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
/**
* @jest-environment node
*
* SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
* SPDX-License-Identifier: BUSL-1.1
* NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
*/
/**
* Unit tests for version-manager.cjs
*
* This file is only meant to be run with Jest.
*/
const path = require('path');
// Mock file system operations - data
const mockPackageJson = { version: '1.2.3' };
const mockVersionJson = {
ios: { build: 100, lastDeployed: '2024-01-01T00:00:00Z' },
android: { build: 200, lastDeployed: '2024-01-01T00:00:00Z' },
};
// Use manual mocking instead of jest.mock to avoid hoisting issues
const fs = require('fs');
// Store originals for restore
const originalReadFileSync = fs.readFileSync;
const originalWriteFileSync = fs.writeFileSync;
const originalExistsSync = fs.existsSync;
const originalAppendFileSync = fs.appendFileSync;
// Setup mocks before importing the module
function setupMocks() {
fs.readFileSync = function (filePath, encoding) {
if (filePath.includes('package.json')) {
return JSON.stringify(mockPackageJson);
}
if (filePath.includes('version.json')) {
return JSON.stringify(mockVersionJson);
}
return originalReadFileSync(filePath, encoding);
};
fs.writeFileSync = function () {};
fs.existsSync = function () {
return true;
};
fs.appendFileSync = function () {};
}
function restoreMocks() {
fs.readFileSync = originalReadFileSync;
fs.writeFileSync = originalWriteFileSync;
fs.existsSync = originalExistsSync;
fs.appendFileSync = originalAppendFileSync;
}
// Setup mocks before requiring the module
setupMocks();
// Import module after mocks are set up
const versionManager = require('./version-manager.cjs');
describe('version-manager', () => {
beforeEach(() => {
// Reset mock data
mockPackageJson.version = '1.2.3';
mockVersionJson.ios.build = 100;
mockVersionJson.android.build = 200;
});
afterAll(() => {
restoreMocks();
});
describe('getVersionInfo', () => {
it('should return current version information', () => {
const info = versionManager.getVersionInfo();
expect(info.version).toBe('1.2.3');
expect(info.iosBuild).toBe(100);
expect(info.androidBuild).toBe(200);
});
});
describe('bumpVersion', () => {
it('should bump major version correctly', () => {
const result = versionManager.bumpVersion('major', 'both');
expect(result.version).toBe('2.0.0');
expect(result.iosBuild).toBe(101);
expect(result.androidBuild).toBe(201);
});
it('should bump minor version correctly', () => {
const result = versionManager.bumpVersion('minor', 'both');
expect(result.version).toBe('1.3.0');
expect(result.iosBuild).toBe(101);
expect(result.androidBuild).toBe(201);
});
it('should bump patch version correctly', () => {
const result = versionManager.bumpVersion('patch', 'both');
expect(result.version).toBe('1.2.4');
expect(result.iosBuild).toBe(101);
expect(result.androidBuild).toBe(201);
});
it('should bump build numbers only', () => {
const result = versionManager.bumpVersion('build', 'both');
expect(result.version).toBe('1.2.3');
expect(result.iosBuild).toBe(101);
expect(result.androidBuild).toBe(201);
});
it('should respect platform parameter (ios only)', () => {
const result = versionManager.bumpVersion('build', 'ios');
expect(result.version).toBe('1.2.3');
expect(result.iosBuild).toBe(101);
expect(result.androidBuild).toBe(200); // unchanged
});
it('should respect platform parameter (android only)', () => {
const result = versionManager.bumpVersion('build', 'android');
expect(result.version).toBe('1.2.3');
expect(result.iosBuild).toBe(100); // unchanged
expect(result.androidBuild).toBe(201);
});
it('should throw on invalid bump type', () => {
expect(() => versionManager.bumpVersion('invalid', 'both')).toThrow(
/Invalid bump type/,
);
});
it('should throw on invalid platform', () => {
expect(() => versionManager.bumpVersion('build', 'invalid')).toThrow(
/Invalid platform/,
);
});
it('should handle version with major bump resetting minor and patch', () => {
mockPackageJson.version = '2.5.8';
const result = versionManager.bumpVersion('major', 'both');
expect(result.version).toBe('3.0.0');
});
it('should handle version with minor bump resetting patch', () => {
mockPackageJson.version = '2.5.8';
const result = versionManager.bumpVersion('minor', 'both');
expect(result.version).toBe('2.6.0');
});
});
describe('applyVersions', () => {
it('should reject invalid version format - not semver', () => {
expect(() => versionManager.applyVersions('invalid', 1, 1)).toThrow(
/Invalid version format/,
);
});
it('should reject invalid version format - two parts', () => {
expect(() => versionManager.applyVersions('1.2', 1, 1)).toThrow(
/Invalid version format/,
);
});
it('should reject invalid version format - four parts', () => {
expect(() => versionManager.applyVersions('1.2.3.4', 1, 1)).toThrow(
/Invalid version format/,
);
});
it('should reject invalid version format - empty string', () => {
expect(() => versionManager.applyVersions('', 1, 1)).toThrow(
/Invalid version format/,
);
});
it('should reject invalid version format - null', () => {
expect(() => versionManager.applyVersions(null, 1, 1)).toThrow(
/Invalid version format/,
);
});
it('should reject invalid iOS build number - zero', () => {
expect(() => versionManager.applyVersions('1.2.3', 0, 1)).toThrow(
/Invalid iOS build/,
);
});
it('should reject invalid iOS build number - negative', () => {
expect(() => versionManager.applyVersions('1.2.3', -1, 1)).toThrow(
/Invalid iOS build/,
);
});
it('should reject invalid iOS build number - non-numeric string', () => {
expect(() => versionManager.applyVersions('1.2.3', 'abc', 1)).toThrow(
/Invalid iOS build/,
);
});
it('should reject invalid iOS build number - float', () => {
expect(() => versionManager.applyVersions('1.2.3', 1.5, 1)).toThrow(
/Invalid iOS build/,
);
});
it('should reject invalid Android build number - zero', () => {
expect(() => versionManager.applyVersions('1.2.3', 1, 0)).toThrow(
/Invalid Android build/,
);
});
it('should reject invalid Android build number - negative', () => {
expect(() => versionManager.applyVersions('1.2.3', 1, -1)).toThrow(
/Invalid Android build/,
);
});
it('should reject invalid Android build number - non-numeric string', () => {
expect(() => versionManager.applyVersions('1.2.3', 1, 'xyz')).toThrow(
/Invalid Android build/,
);
});
it('should reject invalid Android build number - float', () => {
expect(() => versionManager.applyVersions('1.2.3', 1, 2.5)).toThrow(
/Invalid Android build/,
);
});
it('should accept string build numbers that parse to integers', () => {
expect(() =>
versionManager.applyVersions('1.2.3', '100', '200'),
).not.toThrow();
});
it('should accept large build numbers', () => {
expect(() =>
versionManager.applyVersions('1.2.3', 99999, 88888),
).not.toThrow();
});
it('should write correct values to files', () => {
// Track write calls
const writeCalls = [];
fs.writeFileSync = function (filePath, content) {
writeCalls.push({ filePath, content });
};
versionManager.applyVersions('2.0.0', 150, 250);
// Verify writes occurred
expect(writeCalls.length).toBe(2);
// Find and verify package.json write
const packageWrite = writeCalls.find(call =>
call.filePath.includes('package.json'),
);
expect(packageWrite).toBeDefined();
const updatedPackage = JSON.parse(packageWrite.content);
expect(updatedPackage.version).toBe('2.0.0');
// Find and verify version.json write
const versionWrite = writeCalls.find(call =>
call.filePath.includes('version.json'),
);
expect(versionWrite).toBeDefined();
const updatedVersion = JSON.parse(versionWrite.content);
expect(updatedVersion.ios.build).toBe(150);
expect(updatedVersion.android.build).toBe(250);
});
});
describe('readPackageJson', () => {
it('should read and parse package.json', () => {
const pkg = versionManager.readPackageJson();
expect(pkg.version).toBe('1.2.3');
});
it('should throw error if file does not exist', () => {
const originalExists = fs.existsSync;
fs.existsSync = function () {
return false;
};
expect(() => versionManager.readPackageJson()).toThrow(
/package.json not found/,
);
fs.existsSync = originalExists;
});
});
describe('readVersionJson', () => {
it('should read and parse version.json', () => {
const version = versionManager.readVersionJson();
expect(version.ios.build).toBe(100);
expect(version.android.build).toBe(200);
});
it('should throw error if file does not exist', () => {
const originalExists = fs.existsSync;
fs.existsSync = function () {
return false;
};
expect(() => versionManager.readVersionJson()).toThrow(
/version.json not found/,
);
fs.existsSync = originalExists;
});
});
});