Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9828bbd
feat: add version management system with build number tracking
Jul 10, 2025
c84e27a
feat: integrate version.json with Fastlane deployment process
Jul 10, 2025
673da06
feat: enhance deploy confirmation with version.json info
Jul 10, 2025
0bf0cac
fix: use ENV variable directly in increment_build_number to avoid sec…
Jul 10, 2025
95768fc
fix: correct xcodeproj path for GitHub Actions workflow
Jul 10, 2025
a89200a
feat: add test mode to workflow for safe testing
Jul 10, 2025
f68b160
fix: use gradle_file_path instead of gradle_file for increment_versio…
hackertron Jul 10, 2025
04ec74a
fix: use gsub to remove ../ prefix for CI compatibility
hackertron Jul 10, 2025
795e58c
chore: remove accidentally committed files
hackertron Jul 10, 2025
1bb3c7e
feat: auto-commit version.json after successful deployment
hackertron Jul 10, 2025
feaca68
feat : update package.json in build step using npm version
hackertron Jul 14, 2025
69a3f42
feat: add comprehensive caching to mobile deployment workflow
hackertron Jul 14, 2025
28c8267
fix: move bundler config after Ruby setup in mobile-setup action
hackertron Jul 14, 2025
555defc
fix: rename cache env vars to avoid Yarn conflicts
hackertron Jul 14, 2025
47cdb5e
fix: remove bundler deployment mode to allow Gemfile updates
hackertron Jul 14, 2025
8d30040
feat: implement strict lock file enforcement (Option 1)
hackertron Jul 14, 2025
0076db0
fix: update Gemfile.lock for CI environment
hackertron Jul 14, 2025
13a76e0
fix: correct yarn.lock path for monorepo workspace
hackertron Jul 14, 2025
d7f5dfc
fix: handle both boolean and string test_mode parameter
hackertron Jul 14, 2025
07a6859
fix: address code review feedback for mobile deployment workflow
hackertron Jul 15, 2025
290cd3d
fix: formatting and linting issues
hackertron Jul 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 53 additions & 4 deletions .github/workflows/mobile-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ on:
- ios
- android
- both
test_mode:
description: "Test mode (skip upload to stores)"
required: false
type: boolean
default: false

jobs:
build-ios:
Expand Down Expand Up @@ -310,8 +315,30 @@ jobs:
echo "Identities in build.keychain:"
security find-identity -v -p codesigning build.keychain || echo "Failed to find identities in build.keychain"
echo "--- Starting Fastlane ---"
echo "🚀 Uploading to App Store Connect/TestFlight..."
bundle exec fastlane ios internal_test --verbose
if [ "${{ github.event.inputs.test_mode }}" == "true" ]; then
echo "🧪 Running in TEST MODE - will skip upload to TestFlight"
bundle exec fastlane ios internal_test --verbose test_mode:true
else
echo "🚀 Uploading to App Store Connect/TestFlight..."
bundle exec fastlane ios internal_test --verbose
fi

- name: Commit version.json changes
if: success() && github.event.inputs.test_mode != 'true'
run: |
cd ${{ github.workspace }}
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

# Check if version.json has changes
if git diff --quiet app/version.json; then
echo "No changes to version.json, skipping commit"
else
git add app/version.json
git commit -m "chore: update version.json after iOS deployment [skip ci]"
git push
echo "✅ Committed version.json changes"
fi

- name: Remove project.pbxproj updates we don't want to commit
if: github.event_name == 'workflow_dispatch' && github.event.inputs.platform != 'android'
Expand Down Expand Up @@ -495,8 +522,30 @@ jobs:
SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
run: |
cd ${{ env.APP_PATH }}
echo "🚀 Uploading to Google Play Internal Testing..."
bundle exec fastlane android internal_test --verbose
if [ "${{ github.event.inputs.test_mode }}" == "true" ]; then
echo "🧪 Running in TEST MODE - will skip upload to Play Store"
bundle exec fastlane android internal_test --verbose test_mode:true
else
echo "🚀 Uploading to Google Play Internal Testing..."
bundle exec fastlane android internal_test --verbose
fi

- name: Commit version.json changes
if: success() && github.event.inputs.test_mode != 'true'
run: |
cd ${{ github.workspace }}
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

# Check if version.json has changes
if git diff --quiet app/version.json; then
echo "No changes to version.json, skipping commit"
else
git add app/version.json
git commit -m "chore: update version.json after Android deployment [skip ci]"
git push
echo "✅ Committed version.json changes"
fi

- name: Get version from package.json
if: github.event_name == 'workflow_dispatch' && github.event.inputs.platform != 'ios'
Expand Down
96 changes: 64 additions & 32 deletions app/fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,30 @@ platform :ios do
end

desc "Push a new build to TestFlight Internal Testing"
lane :internal_test do
lane :internal_test do |options|
test_mode = options[:test_mode] == "true"

result = prepare_ios_build(prod_release: false)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it intended to let prod_release: false here?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, the production mode is in the other PR


upload_to_testflight(
api_key: result[:api_key],
distribute_external: true,
# Only external TestFlight groups are valid here
groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","),
changelog: "",
skip_waiting_for_build_processing: false,
) if result[:should_upload]
if test_mode
UI.important("🧪 TEST MODE: Skipping TestFlight upload")
UI.success("✅ Build completed successfully!")
UI.message("📦 IPA path: #{result[:ipa_path]}")
else
upload_to_testflight(
api_key: result[:api_key],
distribute_external: true,
# Only external TestFlight groups are valid here
groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","),
changelog: "",
skip_waiting_for_build_processing: false,
) if result[:should_upload]
end

# Update deployment timestamp in version.json
if result[:should_upload]
Fastlane::Helpers.update_deployment_timestamp('ios')
end

# Notify Slack about the new build
if ENV["SLACK_CHANNEL_ID"]
Expand Down Expand Up @@ -122,13 +135,16 @@ platform :ios do

Fastlane::Helpers.verify_env_vars(required_env_vars)

# Get current build number without auto-incrementing
project = Xcodeproj::Project.open(ios_xcode_profile_path)
target = project.targets.first
config = target.build_configurations.first
build_number = config.build_settings["CURRENT_PROJECT_VERSION"]

# Verify build number is higher than TestFlight (but don't auto-increment)
# Get build number from version.json and increment it
build_number = Fastlane::Helpers.bump_ios_build_number

# Update Xcode project with new build number
increment_build_number(
build_number: build_number,
xcodeproj: "ios/#{ENV["IOS_PROJECT_NAME"]}.xcodeproj"
)

# Verify build number is higher than TestFlight
Fastlane::Helpers.ios_verify_app_store_build_number(ios_xcode_profile_path)
Fastlane::Helpers.ios_verify_provisioning_profile

Expand Down Expand Up @@ -200,8 +216,8 @@ platform :android do
end

desc "Push a new build to Google Play Internal Testing"
lane :internal_test do
upload_android_build(track: "internal")
lane :internal_test do |options|
upload_android_build(track: "internal", test_mode: options[:test_mode])
end

desc "Push a new build to Google Play Store"
Expand All @@ -210,6 +226,7 @@ platform :android do
end

private_lane :upload_android_build do |options|
test_mode = options[:test_mode] == "true"
if local_development
if ENV["ANDROID_KEYSTORE_PATH"].nil?
ENV["ANDROID_KEYSTORE_PATH"] = Fastlane::Helpers.android_create_keystore(android_keystore_path)
Expand All @@ -232,10 +249,14 @@ platform :android do

Fastlane::Helpers.verify_env_vars(required_env_vars)

# Get current version code without auto-incrementing
content = File.read(android_gradle_file_path)
match = content.match(/versionCode\s+(\d+)/)
version_code = match ? match[1].to_i : 1
# Get version code from version.json and increment it
version_code = Fastlane::Helpers.bump_android_build_number

# Update build.gradle with new version code
increment_version_code(
version_code: version_code,
gradle_file_path: android_gradle_file_path.gsub("../", "")
)

# TODO: uncomment when we have the permissions to run this action
# Fastlane::Helpers.android_verify_version_code(android_gradle_file_path)
Expand All @@ -260,16 +281,27 @@ platform :android do
)
end

upload_to_play_store(
track: options[:track],
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
package_name: ENV["ANDROID_PACKAGE_NAME"],
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
track_promote_release_status: "completed",
aab: android_aab_path,
) if should_upload && android_has_permissions
if test_mode
UI.important("🧪 TEST MODE: Skipping Play Store upload")
UI.success("✅ Build completed successfully!")
UI.message("📦 AAB path: #{android_aab_path}")
else
upload_to_play_store(
track: options[:track],
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
package_name: ENV["ANDROID_PACKAGE_NAME"],
skip_upload_changelogs: true,
skip_upload_images: true,
skip_upload_screenshots: true,
track_promote_release_status: "completed",
aab: android_aab_path,
) if should_upload && android_has_permissions
end

# Update deployment timestamp in version.json
if should_upload
Fastlane::Helpers.update_deployment_timestamp('android')
end

# Notify Slack about the new build
if ENV["SLACK_CHANNEL_ID"]
Expand Down
2 changes: 2 additions & 0 deletions app/fastlane/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
require_relative "helpers/ios"
require_relative "helpers/android"
require_relative "helpers/slack"
require_relative "helpers/version_manager"

module Fastlane
module Helpers
extend Fastlane::Helpers::Common
extend Fastlane::Helpers::IOS
extend Fastlane::Helpers::Android
extend Fastlane::Helpers::Slack
extend Fastlane::Helpers::VersionManager
end
end

Expand Down
97 changes: 97 additions & 0 deletions app/fastlane/helpers/version_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# SPDX-License-Identifier: BUSL-1.1
require 'json'
require 'time'

module Fastlane
module Helpers
module VersionManager
extend self

VERSION_FILE_PATH = File.expand_path('../../version.json', __dir__)

def read_version_file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we could order the methods by platform: both, ios, android

this is already merged so no action

unless File.exist?(VERSION_FILE_PATH)
UI.user_error!("version.json not found at #{VERSION_FILE_PATH}")
end

JSON.parse(File.read(VERSION_FILE_PATH))
rescue JSON::ParserError => e
UI.user_error!("Failed to parse version.json: #{e.message}")
end

def write_version_file(data)
File.write(VERSION_FILE_PATH, JSON.pretty_generate(data) + "\n")
UI.success("Updated version.json")
rescue => e
UI.user_error!("Failed to write version.json: #{e.message}")
end

def get_current_version
# Version comes from package.json, not version.json
package_json_path = File.expand_path('../../package.json', __dir__)
package_data = JSON.parse(File.read(package_json_path))
package_data['version']
end

def get_ios_build_number
data = read_version_file
data['ios']['build']
end

def get_android_build_number
data = read_version_file
data['android']['build']
end

def bump_ios_build_number
data = read_version_file
current = data['ios']['build']
data['ios']['build'] = current + 1
write_version_file(data)
UI.success("iOS build number bumped from #{current} to #{data['ios']['build']}")
data['ios']['build']
end

def bump_android_build_number
data = read_version_file
current = data['android']['build']
data['android']['build'] = current + 1
write_version_file(data)
UI.success("Android build number bumped from #{current} to #{data['android']['build']}")
data['android']['build']
end

def update_deployment_timestamp(platform)
data = read_version_file
timestamp = Time.now.utc.iso8601

case platform
when 'ios'
data['ios']['lastDeployed'] = timestamp
when 'android'
data['android']['lastDeployed'] = timestamp
else
UI.user_error!("Invalid platform: #{platform}")
end

write_version_file(data)
UI.success("Updated #{platform} deployment timestamp")
end

def sync_build_numbers_to_native
data = read_version_file
version = get_current_version

UI.message("Version #{version} (from package.json)")
UI.message("iOS build: #{data['ios']['build']}")
UI.message("Android build: #{data['android']['build']}")

# Return the build numbers for use in Fastlane
{
ios: data['ios']['build'],
android: data['android']['build']
}
end
end
end
end
Loading