Skip to content

Instantly share code, notes, and snippets.

@cristiangu
Last active March 29, 2025 12:49
Update an APK file with a new React Native JS bundle
module Fastlane
module Actions
module SharedValues
REFRESH_APK_JS_BUNDLE_CUSTOM_VALUE = :REFRESH_APK_JS_BUNDLE_CUSTOM_VALUE
end
class RefreshApkJsBundleAction < Action
def self.run(params)
# fastlane will take care of reading in the parameter and fetching the environment variable:
require 'fastlane/actions/gradle'
sh('bash ./fastlane/install-tools-android.sh')
unzipped_path = "./mobile_app"
js_bundle_path = "#{unzipped_path}/assets/index.android.bundle"
sh("mv ./android/app/build/outputs/apk/release/mobile_app.apk ./mobile_app.apk")
sh("rm -rf ./android/app/build/outputs/apk/release/mobile_app.apk")
sh("apktool d mobile_app.apk")
sh("rm -rf mobile_app.apk")
sh("rm -rf #{js_bundle_path}")
UI.message("Creating a new unminified JS bundle")
sh("yarn react-native bundle \
--dev false \
--minify false \
--platform android \
--entry-file index.js \
--reset-cache \
--bundle-output index.android.bundle \
--sourcemap-output index.android.bundle.map")
UI.message("Compiling JS bundle to Hermes bytecode")
sh("node_modules/react-native/sdks/hermesc/linux64-bin/hermesc \
-O -emit-binary \
-output-source-map \
-out=index.android.bundle.hbc \
index.android.bundle")
sh("rm -f index.android.bundle")
sh("mv index.android.bundle.hbc index.android.bundle")
UI.message("Copying JS bundle to APK assets")
sh("cp index.android.bundle #{js_bundle_path}")
# Upload the new source maps to a crash reporting tool.
#other_action.sentry_upload_sourcemaps(
# bundle_name: "index.android.bundle",
# release: "mobile-app@#{ENV["APP_VERSION"]}+#{params[:build_number]}",
#)
UI.message("Setting app version and build number")
UI.message("App version: #{ENV["APP_VERSION"]}")
UI.message("Build number: #{params[:build_number]}")
sh("cd #{unzipped_path} && yq -i '.versionInfo.versionName = \"#{ENV["APP_VERSION"]}\"' apktool.yml")
sh("cd #{unzipped_path} && yq -i '.versionInfo.versionCode = #{params[:build_number]}' apktool.yml")
UI.message("Zipping APK")
sh("apktool b #{unzipped_path} -o mobile_app.apk")
UI.message("Resigning APK")
# TODO: Update to zipalign -P 16 when upgrading to a newer React Native
# https://github.com/facebook/react-native/issues/45054
sh("zipalign -P 4 -f -v 4 ./mobile_app.apk ./mobile_app-aligned.apk")
sh("rm -rf ./mobile_app.apk")
sh("mv './mobile_app-aligned.apk' './mobile_app.apk'")
# Make sure to use the same signing configuration as the one used in the build.gradle
sh("apksigner sign -v --v2-signing-enabled true --ks ./android/app/release.keystore --ks-key-alias #{ENV['ANDROID_KEYSTORE_ALIAS']} --ks-pass pass:'#{ENV['ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD']}' --key-pass pass:'#{ENV['ANDROID_KEYSTORE_PASSWORD']}' mobile_app.apk")
# Copy APK to the android build folder so it can cached by the CI at the end of the build
sh("cp ./mobile_app.apk ./android/app/build/outputs/apk/release/mobile_app.apk")
end
#####################################################
# @!group Documentation
#####################################################
def self.description
'A short description with <= 80 characters of what this action does'
end
def self.details
# Optional:
# this is your chance to provide a more detailed description of this action
'You can use this action to do cool things...'
end
def self.available_options
# Define all options your action supports.
# Below a few examples
[
FastlaneCore::ConfigItem.new(key: :build_number,
# The name of the environment variable
env_name: 'FL_REFRESH_APK_JS_BUNDLE_BUILD_NUMBER',
# a short description of this parameter
description: 'Build number for the new APK',
verify_block: proc do |value|
unless value && !value.empty?
UI.user_error!("No build number for RefreshIpaJsBundleAction given, pass using `build_number: '231'`")
end
# UI.user_error!("Couldn't find file at path '#{value}'") unless File.exist?(value)
end),
]
end
def self.output
# Define the shared values you are going to provide
# Example
[
['REFRESH_APK_JS_BUNDLE_CUSTOM_VALUE', 'A description of what this value contains']
]
end
def self.return_value
# If your method provides a return value, you can describe here what it does
end
def self.authors
# So no one will ever forget your contribution to fastlane :) You are awesome btw!
['https://github.com/cristiangu']
end
def self.is_supported?(platform)
platform == :android
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment