mirror of
https://github.com/selfxyz/self.git
synced 2026-01-09 14:48:06 -05:00
feat: automated mobile deployments rd2 (#498)
* migrate build logic from previous branch * fix command * move .actrc file * clean up * use env vars * setup provisioning profile path within action * fix flow * fix fastfile flow and update react native * disable play store uploading * hard code xcode version * fixes * more provisioning debugging * fix keychain path * set keychain to what was created by the github action * attempt to build again * test fix * print xcode build settings * debug ios build * fix xcargs path * use manual code signing * save wip * fix building locally * fix variable * save wip * clean up long comand * clean up * install bundle and gems * install pods * fix pod installation * sort * better naming * fix android issues * update lock * clean up artifacts * format * save wip slack upload logic * prettier * fix indent * save wip * save wip * save wip * save wip * save wip * clean up * simplify slack calls * revert slack logic * save working slack upload example * make title nicer * clean up slack upload * upload paths * enable github commit * fix path * fix commit step * fix git committing * update markdown * fix git commit rule * better commit message * chore: incrementing ios build number for version 2.4.9 [skip ci] * better name --------- Co-authored-by: Self GitHub Actions <action@github.com>
This commit is contained in:
3
.actrc
Normal file
3
.actrc
Normal file
@@ -0,0 +1,3 @@
|
||||
--container-architecture linux/amd64
|
||||
--platform macos-latest=catthehacker/ubuntu:runner-latest
|
||||
--secret-file ./app/fastlane/.env.secrets
|
||||
17
.github/actions/get-version/action.yml
vendored
Normal file
17
.github/actions/get-version/action.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Get Version from package.json
|
||||
|
||||
description: "Gets the version from package.json and sets it as an environment variable"
|
||||
|
||||
inputs:
|
||||
app_path:
|
||||
description: "Path to the app directory"
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Get version from package.json
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(node -p "require('${{ inputs.app_path }}/package.json').version")
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
56
.github/actions/mobile-setup/action.yml
vendored
Normal file
56
.github/actions/mobile-setup/action.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Setup Mobile Environment
|
||||
|
||||
description: "Sets up the environment for mobile app builds"
|
||||
|
||||
inputs:
|
||||
app_path:
|
||||
description: "Path to the app directory"
|
||||
required: true
|
||||
node_version:
|
||||
description: "Node version"
|
||||
required: true
|
||||
ruby_version:
|
||||
description: "Ruby version"
|
||||
required: true
|
||||
workspace:
|
||||
description: "Workspace directory path"
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install locales and dialog for local development
|
||||
if: ${{ env.ACT }}
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y locales dialog unzip
|
||||
|
||||
# for fastlane
|
||||
- name: Install locales and dialog
|
||||
if: runner.os != 'macOS'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo locale-gen en_US.UTF-8
|
||||
sudo update-locale LANG=en_US.UTF-8
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Ruby environment
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ inputs.ruby_version }}
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
|
||||
- name: Install app dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
cd ${{ inputs.app_path }}
|
||||
corepack enable
|
||||
yarn install
|
||||
yarn install-app:deploy
|
||||
87
.github/actions/push-changes/action.yml
vendored
Normal file
87
.github/actions/push-changes/action.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Push Build Version Changes
|
||||
|
||||
description: "Commits and pushes build version changes for mobile platforms"
|
||||
|
||||
inputs:
|
||||
commit_message:
|
||||
description: "Commit message"
|
||||
required: true
|
||||
commit_paths:
|
||||
description: "Space-separated list of paths to check for changes (e.g. 'ios/file.txt android/another/file.xml')"
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Configure Git
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
git config --global user.email "action@github.com"
|
||||
git config --global user.name "Self GitHub Actions"
|
||||
|
||||
- name: Commit Changes
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
set -x
|
||||
|
||||
# Restore the logic for checking specific paths existence
|
||||
commit_paths_input="${{ inputs.commit_paths }}"
|
||||
paths_to_commit=""
|
||||
|
||||
for path in $commit_paths_input; do
|
||||
if [ ! -e "$path" ]; then
|
||||
echo "Error: Path $path does not exist"
|
||||
exit 1
|
||||
fi
|
||||
paths_to_commit="$paths_to_commit $path"
|
||||
done
|
||||
|
||||
if [ -z "$paths_to_commit" ]; then
|
||||
echo "No valid paths provided."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove leading space if present
|
||||
paths_to_commit=$(echo "$paths_to_commit" | sed 's/^ *//')
|
||||
|
||||
# Stage ONLY the specified paths
|
||||
git add $paths_to_commit
|
||||
|
||||
# Check if there are staged changes ONLY in the specified paths
|
||||
if git diff --staged --quiet -- $paths_to_commit; then
|
||||
echo "No changes to commit in the specified paths: $paths_to_commit"
|
||||
else
|
||||
echo "Changes to be committed in paths: $paths_to_commit"
|
||||
# Show the staged diff for the specified paths
|
||||
git diff --cached -- $paths_to_commit
|
||||
git commit -m "chore: ${{ inputs.commit_message }} [github action]"
|
||||
fi
|
||||
|
||||
- name: Push Changes
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
set -x
|
||||
|
||||
if git rev-parse --verify HEAD >/dev/null 2>&1; then
|
||||
if [[ ${{ github.ref }} == refs/pull/* ]]; then
|
||||
CURRENT_BRANCH=${{ github.head_ref }}
|
||||
else
|
||||
CURRENT_BRANCH=$(echo ${{ github.ref }} | sed 's|refs/heads/||')
|
||||
fi
|
||||
|
||||
echo "Pushing changes to branch: $CURRENT_BRANCH"
|
||||
# Add --autostash to handle potential unstaged changes gracefully
|
||||
git pull --rebase --autostash origin $CURRENT_BRANCH || {
|
||||
echo "Failed to pull from $CURRENT_BRANCH"
|
||||
exit 1
|
||||
}
|
||||
git push origin HEAD:$CURRENT_BRANCH || {
|
||||
echo "Failed to push to $CURRENT_BRANCH"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo "No new commits to push"
|
||||
fi
|
||||
486
.github/workflows/mobile-deploy.yml
vendored
Normal file
486
.github/workflows/mobile-deploy.yml
vendored
Normal file
@@ -0,0 +1,486 @@
|
||||
name: Mobile App Deployments
|
||||
|
||||
env:
|
||||
# Branch configuration
|
||||
IS_PR: ${{ github.event.pull_request.number != null }}
|
||||
STAGING_BRANCH: dev
|
||||
MAIN_BRANCH: main
|
||||
|
||||
# Build environment versions
|
||||
NODE_VERSION: 18
|
||||
RUBY_VERSION: 3.2
|
||||
JAVA_VERSION: 17
|
||||
ANDROID_API_LEVEL: 35
|
||||
ANDROID_NDK_VERSION: 26.1.10909125
|
||||
|
||||
# Path configuration
|
||||
WORKSPACE: ${{ github.workspace }}
|
||||
APP_PATH: ${{ github.workspace }}/app
|
||||
|
||||
# Certificate/keystore paths
|
||||
ANDROID_KEYSTORE_PATH: /android/app/upload-keystore.jks
|
||||
ANDROID_PLAY_STORE_JSON_KEY_PATH: /android/play-store-key.json
|
||||
IOS_DIST_CERT_PATH: /ios/certs/dist_cert.p12
|
||||
IOS_CONNECT_API_KEY_PATH: /ios/certs/connect_api_key.p8
|
||||
IOS_PROV_PROFILE_PROJ_PATH: /ios/certs/profile.mobileprovision
|
||||
IOS_PROV_PROFILE_DIRECTORY: "~/Library/MobileDevice/Provisioning\ Profiles/"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
paths:
|
||||
- "app/**"
|
||||
- ".github/workflows/mobile-deploy.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "app/**"
|
||||
- ".github/workflows/mobile-deploy.yml"
|
||||
|
||||
jobs:
|
||||
build-ios:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Set up Xcode
|
||||
uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
# some cocoapods won't compile with xcode 16.3
|
||||
xcode-version: "16.2"
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Mobile Dependencies
|
||||
uses: ./.github/actions/mobile-setup
|
||||
with:
|
||||
app_path: ${{ env.APP_PATH }}
|
||||
node_version: ${{ env.NODE_VERSION }}
|
||||
ruby_version: ${{ env.RUBY_VERSION }}
|
||||
workspace: ${{ env.WORKSPACE }}
|
||||
|
||||
- name: Verify iOS Secrets
|
||||
run: |
|
||||
# Verify App Store Connect API Key exists and contains PEM header
|
||||
if [ -z "${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}" ]; then
|
||||
echo "❌ Error: App Store Connect API Key cannot be empty"
|
||||
exit 1
|
||||
fi
|
||||
# Verify Issuer ID is in correct format (UUID)
|
||||
if ! echo "${{ secrets.IOS_CONNECT_ISSUER_ID }}" | grep -E "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" >/dev/null; then
|
||||
echo "❌ Error: Invalid App Store Connect Issuer ID format (should be UUID)"
|
||||
exit 1
|
||||
fi
|
||||
# Verify Key ID is in correct format (alphanumeric)
|
||||
if ! echo "${{ secrets.IOS_CONNECT_KEY_ID }}" | grep -E "^[A-Z0-9]{10}$" >/dev/null; then
|
||||
echo "❌ Error: Invalid App Store Connect Key ID format"
|
||||
exit 1
|
||||
fi
|
||||
# Verify P12 password is not empty and meets basic security requirements
|
||||
if [ -z "${{ secrets.IOS_P12_PASSWORD }}" ]; then
|
||||
echo "❌ Error: P12 password cannot be empty"
|
||||
exit 1
|
||||
fi
|
||||
# Verify base64 secrets are not empty
|
||||
if [ -z "${{ secrets.IOS_DIST_CERT_BASE64 }}" ]; then
|
||||
echo "❌ Error: Distribution certificate cannot be empty"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "${{ secrets.IOS_PROV_PROFILE_BASE64 }}" ]; then
|
||||
echo "❌ Error: Provisioning profile cannot be empty"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ All iOS secrets verified successfully!"
|
||||
|
||||
- name: Decode certificate and profile
|
||||
run: |
|
||||
mkdir -p "${{ env.APP_PATH }}$(dirname "${{ env.IOS_DIST_CERT_PATH }}")"
|
||||
echo "${{ secrets.IOS_DIST_CERT_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}
|
||||
echo "${{ secrets.IOS_PROV_PROFILE_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}
|
||||
echo "${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.IOS_CONNECT_API_KEY_PATH }}
|
||||
|
||||
# for debugging...which can take some time :(
|
||||
- name: Verify ios secret checksums
|
||||
if: false # for debugging
|
||||
run: |
|
||||
echo "SHA256 of dist_cert.p12:"
|
||||
shasum -a 256 ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}
|
||||
echo "SHA256 of profile.mobileprovision:"
|
||||
shasum -a 256 ${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}
|
||||
echo "SHA256 of connect_api_key.p8:"
|
||||
shasum -a 256 ${{ env.APP_PATH }}${{ env.IOS_CONNECT_API_KEY_PATH }}
|
||||
echo "Certificate file size:"
|
||||
ls -l ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}
|
||||
echo "SHA256 of password:"
|
||||
echo -n "${{ secrets.IOS_P12_PASSWORD }}" | shasum -a 256
|
||||
echo "SHA256 of connect_api_key_base64:"
|
||||
echo -n "${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}" | shasum -a 256
|
||||
echo "Verifying certificate..."
|
||||
if openssl pkcs12 -in ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} -password pass:'${{ secrets.IOS_P12_PASSWORD }}' -info >/dev/null 2>&1 || openssl pkcs12 -in ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} -password pass:'${{ secrets.IOS_P12_PASSWORD }}' -info 2>&1 | grep -q "MAC:"; then
|
||||
echo "✅ Certificate verification successful (algorithm warning can be safely ignored)"
|
||||
else
|
||||
echo "❌ Certificate verification failed - please check certificate validity"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify iOS certificate and environment
|
||||
if: ${{ !env.ACT }}
|
||||
run: |
|
||||
# Check if certificate directory exists
|
||||
if [ ! -d "${{ env.APP_PATH }}/ios/certs" ]; then
|
||||
echo "❌ Error: iOS certificates directory not found at ${{ env.APP_PATH }}/ios/certs"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if certificate file exists
|
||||
if [ ! -f "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}" ]; then
|
||||
echo "❌ Error: Distribution certificate not found at ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check certificate file permissions
|
||||
CERT_PERMS=$(ls -l "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}" | awk '{print $1}')
|
||||
if [ "$CERT_PERMS" != "-rw-r--r--" ]; then
|
||||
echo "❌ Error: Distribution certificate has incorrect permissions: $CERT_PERMS"
|
||||
echo "Expected: -rw-r--r--"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check certificate file size
|
||||
CERT_SIZE=$(stat -f%z "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}" 2>/dev/null || stat -c%s "${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }}")
|
||||
if [ "$CERT_SIZE" -lt 1000 ]; then
|
||||
echo "❌ Error: Distribution certificate file size ($CERT_SIZE bytes) is suspiciously small"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we can create a test keychain
|
||||
TEST_KEYCHAIN="test.keychain"
|
||||
if ! security create-keychain -p "" "$TEST_KEYCHAIN" >/dev/null 2>&1; then
|
||||
echo "❌ Error: Unable to create test keychain. Check permissions."
|
||||
exit 1
|
||||
fi
|
||||
security delete-keychain "$TEST_KEYCHAIN" >/dev/null 2>&1
|
||||
|
||||
echo "✅ Certificate and environment verification passed!"
|
||||
|
||||
- name: Install certificate
|
||||
if: ${{ !env.ACT }}
|
||||
run: |
|
||||
security create-keychain -p "" build.keychain >/dev/null 2>&1
|
||||
security default-keychain -s build.keychain >/dev/null 2>&1
|
||||
security unlock-keychain -p "" build.keychain >/dev/null 2>&1
|
||||
security import ${{ env.APP_PATH }}${{ env.IOS_DIST_CERT_PATH }} -k build.keychain -P '${{ secrets.IOS_P12_PASSWORD }}' -T /usr/bin/codesign >/dev/null 2>&1
|
||||
security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain >/dev/null 2>&1
|
||||
|
||||
- name: Install provisioning profile
|
||||
if: ${{ !env.ACT }}
|
||||
env:
|
||||
IOS_APP_IDENTIFIER: ${{ secrets.IOS_APP_IDENTIFIER }}
|
||||
IOS_PROV_PROFILE_NAME: ${{ secrets.IOS_PROV_PROFILE_NAME }}
|
||||
IOS_PROV_PROFILE_PATH: ${{ env.IOS_PROV_PROFILE_PATH }}
|
||||
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
|
||||
run: |
|
||||
# Verify file exists before proceeding
|
||||
echo "Checking for provisioning profile at: ${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}"
|
||||
ls -l "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}"
|
||||
if [ ! -f "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" ]; then
|
||||
echo "❌ Error: Provisioning profile not found at specified path."
|
||||
exit 1
|
||||
fi
|
||||
echo "Provisioning profile found."
|
||||
|
||||
# Print file details
|
||||
echo "Provisioning Profile File Details:"
|
||||
echo "--------------------------------"
|
||||
echo "File size: $(stat -f%z "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" 2>/dev/null || stat -c%s "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}") bytes"
|
||||
echo "File permissions: $(ls -l "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" | awk '{print $1}')"
|
||||
echo "File owner: $(ls -l "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" | awk '{print $3}')"
|
||||
echo "--------------------------------"
|
||||
|
||||
# Create a temporary plist file to extract UUID
|
||||
TEMP_PLIST_PATH=$(mktemp /tmp/profile_plist.XXXXXX)
|
||||
|
||||
# Extract plist from mobileprovision file
|
||||
echo "Extracting plist from provisioning profile..."
|
||||
security cms -D -i "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" -o "$TEMP_PLIST_PATH"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Error: Failed to extract plist from provisioning profile"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract UUID and profile name from plist
|
||||
echo "Extracting UUID and profile name from plist..."
|
||||
PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print :UUID" "$TEMP_PLIST_PATH" 2>/dev/null)
|
||||
if [ $? -ne 0 ] || [ -z "$PROFILE_UUID" ]; then
|
||||
echo "❌ Error: Failed to extract UUID from provisioning profile"
|
||||
cat "$TEMP_PLIST_PATH" | head -20
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the actual profile name from within the file
|
||||
PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print :Name" "$TEMP_PLIST_PATH" 2>/dev/null)
|
||||
if [ $? -ne 0 ] || [ -z "$PROFILE_NAME" ]; then
|
||||
echo "⚠️ Warning: Failed to extract Name from provisioning profile, will use provided IOS_PROV_PROFILE_NAME"
|
||||
PROFILE_NAME="$IOS_PROV_PROFILE_NAME"
|
||||
fi
|
||||
|
||||
echo "Profile UUID: $PROFILE_UUID"
|
||||
echo "Profile Name: $PROFILE_NAME"
|
||||
|
||||
# Install provisioning profile in the correct location with UUID filename
|
||||
echo "Installing provisioning profile in filesystem..."
|
||||
mkdir -p "/Users/runner/Library/MobileDevice/Provisioning Profiles"
|
||||
|
||||
# Copy with the UUID as filename
|
||||
UUID_TARGET_PATH="/Users/runner/Library/MobileDevice/Provisioning Profiles/${PROFILE_UUID}.mobileprovision"
|
||||
cp -v "${{ env.APP_PATH }}${{ env.IOS_PROV_PROFILE_PROJ_PATH }}" "$UUID_TARGET_PATH"
|
||||
|
||||
# Set correct permissions on the profile
|
||||
chmod 644 "$UUID_TARGET_PATH"
|
||||
chown runner:staff "$UUID_TARGET_PATH"
|
||||
|
||||
# Save the profile path and name to environment for later steps
|
||||
echo "IOS_PROV_PROFILE_PATH=$UUID_TARGET_PATH" >> $GITHUB_ENV
|
||||
echo "IOS_PROV_PROFILE_NAME=$PROFILE_NAME" >> $GITHUB_ENV
|
||||
|
||||
# Print provisioning profile information
|
||||
echo "Provisioning Profile Information:"
|
||||
echo "--------------------------------"
|
||||
echo "Profile Name (from file): $PROFILE_NAME"
|
||||
echo "Profile Name (from env): $IOS_PROV_PROFILE_NAME"
|
||||
echo "Profile Path: $UUID_TARGET_PATH"
|
||||
echo "Profile UUID: $PROFILE_UUID"
|
||||
echo "App Identifier: $IOS_APP_IDENTIFIER"
|
||||
echo "Team ID: $IOS_TEAM_ID"
|
||||
echo "--------------------------------"
|
||||
|
||||
# List all provisioning profiles in the system with detailed info
|
||||
echo "List of all provisioning profiles in system:"
|
||||
ls -la "/Users/runner/Library/MobileDevice/Provisioning Profiles/"
|
||||
|
||||
# Clean up temp file
|
||||
rm -f "$TEMP_PLIST_PATH"
|
||||
|
||||
echo "✅ Provisioning profile installation steps completed."
|
||||
|
||||
# act won't work with macos, but you can test with `bundle exec fastlane ios ...`
|
||||
- name: Build and upload to TestFlight (Internal)
|
||||
if: ${{ !env.ACT }}
|
||||
env:
|
||||
IS_PR: ${{ env.IS_PR }}
|
||||
IOS_APP_IDENTIFIER: ${{ secrets.IOS_APP_IDENTIFIER }}
|
||||
IOS_CONNECT_API_KEY_BASE64: ${{ secrets.IOS_CONNECT_API_KEY_BASE64 }}
|
||||
IOS_CONNECT_API_KEY_PATH: ${{ env.APP_PATH }}${{ env.IOS_CONNECT_API_KEY_PATH }}
|
||||
IOS_CONNECT_ISSUER_ID: ${{ secrets.IOS_CONNECT_ISSUER_ID }}
|
||||
IOS_CONNECT_KEY_ID: ${{ secrets.IOS_CONNECT_KEY_ID }}
|
||||
IOS_P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
|
||||
IOS_PROJECT_NAME: ${{ secrets.IOS_PROJECT_NAME }}
|
||||
IOS_PROJECT_SCHEME: ${{ secrets.IOS_PROJECT_SCHEME }}
|
||||
IOS_PROV_PROFILE_DIR: ${{ env.IOS_PROV_PROFILE_DIRECTORY }}
|
||||
IOS_PROV_PROFILE_NAME: ${{ secrets.IOS_PROV_PROFILE_NAME }}
|
||||
IOS_PROV_PROFILE_PATH: ${{ env.IOS_PROV_PROFILE_PATH }}
|
||||
IOS_SIGNING_CERTIFICATE: ${{ secrets.IOS_SIGNING_CERTIFICATE }}
|
||||
IOS_TESTFLIGHT_GROUPS: ${{ secrets.IOS_TESTFLIGHT_GROUPS }}
|
||||
IOS_TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
|
||||
IOS_TEAM_NAME: ${{ secrets.IOS_TEAM_NAME }}
|
||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
|
||||
SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
|
||||
timeout-minutes: 90
|
||||
run: |
|
||||
cd ${{ env.APP_PATH }}
|
||||
echo "--- Pre-Fastlane Diagnostics ---"
|
||||
echo "Running as user: $(whoami)"
|
||||
echo "Default keychain:"
|
||||
security list-keychains -d user
|
||||
echo "Identities in build.keychain:"
|
||||
security find-identity -v -p codesigning build.keychain || echo "Failed to find identities in build.keychain"
|
||||
echo "--- Starting Fastlane ---"
|
||||
# if pushing to main, deploy to App Store
|
||||
if [ "${{ github.ref }}" = "refs/heads/${{ env.MAIN_BRANCH }}" ]; then
|
||||
bundle exec fastlane ios deploy --verbose
|
||||
# else to upload to TestFlight Internal Testing
|
||||
else
|
||||
bundle exec fastlane ios internal_test --verbose
|
||||
fi
|
||||
|
||||
- name: Remove project.pbxproj updates we don't want to commit
|
||||
run: |
|
||||
PBXPROJ_FILE="app/ios/Self.xcodeproj/project.pbxproj"
|
||||
|
||||
# Create a temporary file to store version info
|
||||
echo "Extracting version information..."
|
||||
rm -f versions.txt
|
||||
grep -E 'CURRENT_PROJECT_VERSION = [0-9]+;|MARKETING_VERSION = [0-9]+\.[0-9]+\.[0-9]+;' "${PBXPROJ_FILE}" > versions.txt
|
||||
|
||||
# Check if we have version information
|
||||
if [ -s versions.txt ]; then
|
||||
echo "Found version information. Resetting file and re-applying versions..."
|
||||
|
||||
# Store the version values
|
||||
CURRENT_VERSION=$(grep 'CURRENT_PROJECT_VERSION' versions.txt | head -1 | sed 's/.*CURRENT_PROJECT_VERSION = \([0-9]*\);.*/\1/')
|
||||
MARKETING_VERSION=$(grep 'MARKETING_VERSION' versions.txt | head -1 | sed 's/.*MARKETING_VERSION = \([0-9]*\.[0-9]*\.[0-9]*\);.*/\1/')
|
||||
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
echo "Marketing version: $MARKETING_VERSION"
|
||||
|
||||
# Reset the file to HEAD
|
||||
git checkout HEAD -- "${PBXPROJ_FILE}"
|
||||
|
||||
# Update the versions if they exist
|
||||
if [ ! -z "$CURRENT_VERSION" ]; then
|
||||
sed -i '' "s/\(CURRENT_PROJECT_VERSION = \)[0-9]*;/\1$CURRENT_VERSION;/g" "${PBXPROJ_FILE}"
|
||||
fi
|
||||
|
||||
if [ ! -z "$MARKETING_VERSION" ]; then
|
||||
sed -i '' "s/\(MARKETING_VERSION = \)[0-9]*\.[0-9]*\.[0-9]*;/\1$MARKETING_VERSION;/g" "${PBXPROJ_FILE}"
|
||||
fi
|
||||
|
||||
echo "Version information successfully applied."
|
||||
else
|
||||
echo "No version information found. Resetting file..."
|
||||
git checkout HEAD -- "${PBXPROJ_FILE}"
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
rm -f versions.txt
|
||||
|
||||
- name: Get version from package.json
|
||||
uses: ./.github/actions/get-version
|
||||
with:
|
||||
app_path: ${{ env.APP_PATH }}
|
||||
|
||||
- name: Commit updated build number
|
||||
if: ${{ !env.ACT }}
|
||||
uses: ./.github/actions/push-changes
|
||||
with:
|
||||
commit_message: "incrementing ios build number for version ${{ env.VERSION }}"
|
||||
commit_paths: "./app/ios/OpenPassport/Info.plist ./app/ios/Self.xcodeproj/project.pbxproj"
|
||||
|
||||
build-android:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Mobile Dependencies
|
||||
uses: ./.github/actions/mobile-setup
|
||||
with:
|
||||
app_path: ${{ env.APP_PATH }}
|
||||
node_version: ${{ env.NODE_VERSION }}
|
||||
ruby_version: ${{ env.RUBY_VERSION }}
|
||||
workspace: ${{ env.WORKSPACE }}
|
||||
|
||||
# android specific steps
|
||||
- name: Setup Java environment
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: "temurin"
|
||||
java-version: ${{ env.JAVA_VERSION }}
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
with:
|
||||
accept-android-sdk-licenses: true
|
||||
|
||||
- name: Install NDK
|
||||
run: |
|
||||
max_attempts=5
|
||||
attempt=1
|
||||
while [ $attempt -le $max_attempts ]; do
|
||||
echo "Attempt $attempt of $max_attempts to install NDK..."
|
||||
if sdkmanager "ndk;${{ env.ANDROID_NDK_VERSION }}"; then
|
||||
echo "Successfully installed NDK"
|
||||
exit 0
|
||||
fi
|
||||
echo "Failed to install NDK on attempt $attempt"
|
||||
if [ $attempt -eq $max_attempts ]; then
|
||||
echo "All attempts to install NDK failed"
|
||||
exit 1
|
||||
fi
|
||||
# Exponential backoff: 2^attempt seconds
|
||||
wait_time=$((2 ** attempt))
|
||||
echo "Waiting $wait_time seconds before retrying..."
|
||||
sleep $wait_time
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
- name: Set Gradle JVM options
|
||||
if: ${{ env.ACT }} # run when testing locally with act to prevent gradle crashes
|
||||
run: |
|
||||
echo "org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8" >> ${{ env.APP_PATH }}/android/gradle.properties
|
||||
|
||||
- name: Decode Android Secrets
|
||||
run: |
|
||||
echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}
|
||||
echo "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" | base64 --decode > ${{ env.APP_PATH }}${{ env.ANDROID_PLAY_STORE_JSON_KEY_PATH }}
|
||||
|
||||
# run secrets check after keytool has been setup
|
||||
- name: Verify Android Secrets
|
||||
run: |
|
||||
# Verify Play Store JSON key base64 secret exists and is valid
|
||||
if [ -z "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" ]; then
|
||||
echo "❌ Error: Play Store JSON key base64 secret cannot be empty"
|
||||
exit 1
|
||||
fi
|
||||
# Verify the base64 can be decoded
|
||||
if ! echo "${{ secrets.ANDROID_PLAY_STORE_JSON_KEY_BASE64 }}" | base64 --decode >/dev/null 2>&1; then
|
||||
echo "❌ Error: Invalid Play Store JSON key base64 format"
|
||||
exit 1
|
||||
fi
|
||||
# Verify keystore file exists and is valid
|
||||
if [ ! -f "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" ]; then
|
||||
echo "❌ Error: Keystore file was not created successfully"
|
||||
exit 1
|
||||
fi
|
||||
# Try to verify the keystore with the provided password
|
||||
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >/dev/null 2>&1; then
|
||||
echo "❌ Error: Invalid keystore password"
|
||||
exit 1
|
||||
fi
|
||||
# Verify the key alias exists
|
||||
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" >/dev/null 2>&1; then
|
||||
echo "❌ Error: Key alias '${{ secrets.ANDROID_KEY_ALIAS }}' not found in keystore"
|
||||
exit 1
|
||||
fi
|
||||
# Verify the key password
|
||||
if ! keytool -list -v -keystore "${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}" -storepass "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" -alias "${{ secrets.ANDROID_KEY_ALIAS }}" -keypass "${{ secrets.ANDROID_KEY_PASSWORD }}" >/dev/null 2>&1; then
|
||||
echo "❌ Error: Invalid key password"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ All Android secrets verified successfully!"
|
||||
|
||||
- name: Build and upload to Google Play Internal Testing
|
||||
env:
|
||||
IS_PR: ${{ env.IS_PR }}
|
||||
ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEYSTORE_PATH: ${{ env.APP_PATH }}${{ env.ANDROID_KEYSTORE_PATH }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
ANDROID_PACKAGE_NAME: ${{ secrets.ANDROID_PACKAGE_NAME }}
|
||||
ANDROID_PLAY_STORE_JSON_KEY_PATH: ${{ env.APP_PATH }}${{ env.ANDROID_PLAY_STORE_JSON_KEY_PATH }}
|
||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||
SLACK_API_TOKEN: ${{ secrets.SLACK_API_TOKEN }}
|
||||
SLACK_ANNOUNCE_CHANNEL_NAME: ${{ secrets.SLACK_ANNOUNCE_CHANNEL_NAME }}
|
||||
run: |
|
||||
cd ${{ env.APP_PATH }}
|
||||
# if pushing to main, deploy to Play Store
|
||||
if [ "${{ github.ref }}" = "refs/heads/${{ env.MAIN_BRANCH }}" ]; then
|
||||
bundle exec fastlane android deploy --verbose
|
||||
# else to upload to Play Store Internal Testing
|
||||
else
|
||||
bundle exec fastlane android internal_test --verbose
|
||||
fi
|
||||
|
||||
- name: Get version from package.json
|
||||
uses: ./.github/actions/get-version
|
||||
with:
|
||||
app_path: ${{ env.APP_PATH }}
|
||||
|
||||
- name: Commit updated build version
|
||||
if: ${{ !env.ACT }}
|
||||
uses: ./.github/actions/push-changes
|
||||
with:
|
||||
commit_message: "incrementing android build version for version ${{ env.VERSION }}"
|
||||
commit_paths: "./app/android/app/build.gradle"
|
||||
5
app/.gitignore
vendored
5
app/.gitignore
vendored
@@ -22,6 +22,7 @@ DerivedData
|
||||
*.xcuserstate
|
||||
ios/.xcode.env.local
|
||||
**/.xcode.env.local
|
||||
ios/certs
|
||||
|
||||
|
||||
# Android/IntelliJ
|
||||
@@ -37,6 +38,9 @@ local.properties
|
||||
# debug bundled builds
|
||||
android/app/src/main/assets/*android.bundle
|
||||
android/app/src/main/res/*/node_modules*
|
||||
android/.kotlin/
|
||||
android/app/upload-keystore.jks
|
||||
android/app/play-store-key.json
|
||||
|
||||
# node.js
|
||||
#
|
||||
@@ -55,6 +59,7 @@ yarn-error.log
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
**/fastlane/test_output
|
||||
**/fastlane/.env.secrets
|
||||
|
||||
# Bundle artifact
|
||||
*.jsbundle
|
||||
|
||||
@@ -5,4 +5,5 @@ node_modules/
|
||||
src/assets/animations/
|
||||
witnesscalc/
|
||||
vendor/
|
||||
android/
|
||||
android/
|
||||
*.md
|
||||
|
||||
1
app/.ruby-version
Normal file
1
app/.ruby-version
Normal file
@@ -0,0 +1 @@
|
||||
3.2.7
|
||||
20
app/Gemfile
20
app/Gemfile
@@ -1,8 +1,20 @@
|
||||
source 'https://rubygems.org'
|
||||
source "https://rubygems.org"
|
||||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby ">= 2.6.10"
|
||||
ruby ">= 3.2"
|
||||
|
||||
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
|
||||
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||
gem "cocoapods", ">= 1.13", "!= 1.15.0", "!= 1.15.1"
|
||||
gem "activesupport", ">= 6.1.7.5", "!= 7.1.0"
|
||||
|
||||
# Add fastlane for CI/CD
|
||||
gem "fastlane", "~> 2.227.0"
|
||||
|
||||
group :development do
|
||||
gem "dotenv"
|
||||
# Exclude nokogiri for GitHub Actions and Act
|
||||
gem "nokogiri", "~> 1.18", platform: :ruby unless ENV["GITHUB_ACTIONS"] || ENV["ACT"]
|
||||
end
|
||||
|
||||
plugins_path = File.join(File.dirname(__FILE__), "fastlane", "Pluginfile")
|
||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||
|
||||
274
app/Gemfile.lock
274
app/Gemfile.lock
@@ -1,27 +1,57 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.6)
|
||||
CFPropertyList (3.0.7)
|
||||
base64
|
||||
nkf
|
||||
rexml
|
||||
activesupport (6.1.7.4)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
activesupport (7.2.2.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
zeitwerk (~> 2.3)
|
||||
addressable (2.8.4)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1069.0)
|
||||
aws-sdk-core (3.220.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.99.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.182.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.9)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.12.1)
|
||||
cocoapods (1.16.2)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.12.1)
|
||||
cocoapods-core (= 1.16.2)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.6.0, < 2.0)
|
||||
cocoapods-downloader (>= 2.1, < 3.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.6.0, < 2.0)
|
||||
@@ -33,8 +63,8 @@ GEM
|
||||
molinillo (~> 0.8.0)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (>= 2.3.0, < 3.0)
|
||||
xcodeproj (>= 1.21.0, < 2.0)
|
||||
cocoapods-core (1.12.1)
|
||||
xcodeproj (>= 1.27.0, < 2.0)
|
||||
cocoapods-core (1.16.2)
|
||||
activesupport (>= 5.0, < 8)
|
||||
addressable (~> 2.8)
|
||||
algoliasearch (~> 1.0)
|
||||
@@ -45,7 +75,7 @@ GEM
|
||||
public_suffix (~> 4.0)
|
||||
typhoeus (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.5)
|
||||
cocoapods-downloader (1.6.3)
|
||||
cocoapods-downloader (2.1)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.1)
|
||||
@@ -53,48 +83,234 @@ GEM
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.2.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
concurrent-ruby (1.3.5)
|
||||
connection_pool (2.5.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
drb (2.2.1)
|
||||
emoji_regex (3.2.3)
|
||||
escape (0.0.4)
|
||||
ethon (0.16.0)
|
||||
ffi (>= 1.15.0)
|
||||
ffi (1.15.5)
|
||||
excon (0.112.0)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.7)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (~> 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.227.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
fastlane-sirp (>= 1.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
naturally (~> 2.2)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.4.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-plugin-increment_version_code (0.4.3)
|
||||
fastlane-plugin-versioning_android (0.1.1)
|
||||
fastlane-sirp (1.0.0)
|
||||
sysrandom (~> 1.0)
|
||||
ffi (1.17.1)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.1)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.3)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.31.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.8.0)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.5.0)
|
||||
google-cloud-storage (1.47.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.31.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.8.1)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.6.3)
|
||||
minitest (5.18.1)
|
||||
jmespath (1.6.2)
|
||||
json (2.10.2)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
logger (1.6.6)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.5)
|
||||
molinillo (0.8.0)
|
||||
nanaimo (0.3.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.1)
|
||||
netrc (0.11.0)
|
||||
nkf (0.2.0)
|
||||
nokogiri (1.18.5)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
public_suffix (4.0.7)
|
||||
rexml (3.2.5)
|
||||
racc (1.8.1)
|
||||
rake (13.2.1)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.1.2)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby-macho (2.5.1)
|
||||
typhoeus (1.4.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
securerandom (0.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
sysrandom (1.0.5)
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
typhoeus (1.4.1)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
xcodeproj (1.22.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.27.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (~> 3.2.4)
|
||||
zeitwerk (2.6.8)
|
||||
nanaimo (~> 0.4.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.4.0)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
cocoapods (~> 1.12)
|
||||
activesupport (>= 6.1.7.5, != 7.1.0)
|
||||
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
|
||||
dotenv
|
||||
fastlane (~> 2.227.0)
|
||||
fastlane-plugin-increment_version_code
|
||||
fastlane-plugin-versioning_android
|
||||
nokogiri (~> 1.18)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 2.6.10p210
|
||||
ruby 3.2.7p253
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.2
|
||||
2.4.19
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "34.0.0"
|
||||
buildToolsVersion = "35.0.0"
|
||||
minSdkVersion = 23
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
compileSdkVersion = 35
|
||||
targetSdkVersion = 35
|
||||
ndkVersion = "26.1.10909125"
|
||||
kotlinVersion = "1.9.24"
|
||||
}
|
||||
|
||||
22
app/fastlane/.env.secrets.example
Normal file
22
app/fastlane/.env.secrets.example
Normal file
@@ -0,0 +1,22 @@
|
||||
ANDROID_KEYSTORE=
|
||||
ANDROID_KEYSTORE_PASSWORD=
|
||||
ANDROID_KEY_ALIAS=
|
||||
ANDROID_KEY_PASSWORD=
|
||||
ANDROID_PACKAGE_NAME=
|
||||
ANDROID_PLAY_STORE_JSON_KEY_BASE64=
|
||||
IOS_APP_IDENTIFIER=
|
||||
IOS_CONNECT_API_KEY_BASE64=
|
||||
IOS_CONNECT_ISSUER_ID=
|
||||
IOS_CONNECT_KEY_ID=
|
||||
IOS_DIST_CERT_BASE64=
|
||||
IOS_PROJECT_NAME=
|
||||
IOS_PROJECT_SCHEME=
|
||||
IOS_PROV_PROFILE_BASE64=
|
||||
IOS_PROV_PROFILE_NAME=
|
||||
IOS_P12_PASSWORD=
|
||||
IOS_SIGNING_CERTIFICATE=
|
||||
IOS_TEAM_ID=
|
||||
IOS_TEAM_NAME=
|
||||
IOS_TESTFLIGHT_GROUPS=
|
||||
SLACK_CHANNEL_ID=
|
||||
SLACK_BOT_TOKEN=
|
||||
377
app/fastlane/DEV.md
Normal file
377
app/fastlane/DEV.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# Fastlane & CI/CD Development Guide 🚀
|
||||
|
||||
This document outlines how to work with the Fastlane setup and the GitHub Actions CI/CD pipeline for this mobile application.
|
||||
|
||||
## Table of Contents
|
||||
- [Prerequisites](#prerequisites-)
|
||||
- [Setup](#setup-)
|
||||
- [Workflow Overview](#workflow-overview-)
|
||||
- [Local Development](#local-development-)
|
||||
- [CI/CD Pipeline](#cicd-pipeline-)
|
||||
- [Version Management](#version-management-)
|
||||
- [Platform-Specific Notes](#platform-specific-notes-)
|
||||
- [Troubleshooting](#troubleshooting-)
|
||||
- [Additional Resources](#additional-resources-)
|
||||
|
||||
## Prerequisites 🛠️
|
||||
|
||||
Before working with this setup, ensure you have the following installed:
|
||||
|
||||
* **Ruby** - Fastlane requires Ruby (version 2.6.0 or higher recommended)
|
||||
* **Bundler** - For managing Ruby dependencies
|
||||
* **Xcode** - For iOS development (latest stable version recommended)
|
||||
* **Android Studio** - For Android development
|
||||
* **Node.js & Yarn** - For JavaScript dependencies
|
||||
* **Docker** - Optional, required for local testing with `act`
|
||||
|
||||
## Setup ⚙️
|
||||
|
||||
### Local Fastlane Setup
|
||||
|
||||
1. Install Fastlane via Bundler:
|
||||
```bash
|
||||
cd app
|
||||
bundle install
|
||||
```
|
||||
|
||||
2. Verify installation:
|
||||
```bash
|
||||
bundle exec fastlane --version
|
||||
```
|
||||
|
||||
### Secrets Management (`.env.secrets`) 🔑
|
||||
|
||||
Fastlane requires various secrets to interact with the app stores and sign applications:
|
||||
|
||||
1. **Create Your Local Secrets File:** Copy the template file to create your secrets file:
|
||||
|
||||
```bash
|
||||
cp app/fastlane/.env.secrets.example app/fastlane/.env.secrets
|
||||
```
|
||||
|
||||
2. **Populate Values:** Fill in the values in your newly created `.env.secrets` file. Obtain these credentials from the appropriate platform developer portals or your team's administrator.
|
||||
|
||||
3. **Keep it Private:** The `.env.secrets` file is included in the project's `.gitignore` and **must not** be committed to the repository.
|
||||
|
||||
4. **CI/CD Setup:** For the GitHub Actions workflow, these same secrets must be configured as [GitHub Actions Secrets](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) in the repository settings.
|
||||
|
||||
### Environment Secrets Reference 📝
|
||||
|
||||
#### Android Secrets 🤖
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `ANDROID_KEYSTORE` | Path to keystore file used for signing Android apps |
|
||||
| `ANDROID_KEYSTORE_PASSWORD` | Password for the Android keystore |
|
||||
| `ANDROID_KEY_ALIAS` | Alias of the key in the keystore |
|
||||
| `ANDROID_KEY_PASSWORD` | Password for the specified key |
|
||||
| `ANDROID_PACKAGE_NAME` | Package name/application ID of the Android app |
|
||||
| `ANDROID_PLAY_STORE_JSON_KEY_BASE64` | Base64 encoded Google Play Store service account JSON key file for API access |
|
||||
|
||||
#### iOS Secrets 🍏
|
||||
|
||||
| Secret | Description |
|
||||
|--------|-------------|
|
||||
| `IOS_APP_IDENTIFIER` | Bundle identifier for the iOS app |
|
||||
| `IOS_CONNECT_API_KEY_BASE64` | Base64 encoded App Store Connect API key for authentication |
|
||||
| `IOS_CONNECT_ISSUER_ID` | App Store Connect issuer ID associated with the API key |
|
||||
| `IOS_CONNECT_KEY_ID` | App Store Connect key ID for API access |
|
||||
| `IOS_DIST_CERT_BASE64` | Base64 encoded iOS distribution certificate for code signing |
|
||||
| `IOS_PROV_PROFILE_BASE64` | Base64 encoded provisioning profile for the app |
|
||||
| `IOS_PROV_PROFILE_NAME` | Name of the provisioning profile |
|
||||
| `IOS_P12_PASSWORD` | Password for the p12 certificate file |
|
||||
| `IOS_TEAM_ID` | Apple Developer Team ID |
|
||||
| `IOS_TEAM_NAME` | Apple Developer Team name |
|
||||
| `IOS_TESTFLIGHT_GROUPS` | Comma-separated list of TestFlight groups to distribute the app to |
|
||||
|
||||
## Workflow Overview 🔄
|
||||
|
||||
### Fastlane Lanes
|
||||
|
||||
The project uses several custom Fastlane lanes to handle different build and deployment scenarios:
|
||||
|
||||
#### iOS Lanes
|
||||
|
||||
| Lane | Description | Usage |
|
||||
|------|-------------|-------|
|
||||
| `internal_test` | Builds a beta version and uploads to TestFlight | `bundle exec fastlane ios internal_test` |
|
||||
| `deploy` | Builds a production version and uploads to App Store Connect | `bundle exec fastlane ios deploy` |
|
||||
| `sync_version` | Syncs version from package.json to Info.plist | `bundle exec fastlane ios sync_version` |
|
||||
|
||||
#### Android Lanes
|
||||
|
||||
| Lane | Description | Usage |
|
||||
|------|-------------|-------|
|
||||
| `internal_test` | Builds a beta version and uploads to Google Play Internal Testing | `bundle exec fastlane android internal_test` |
|
||||
| `deploy` | Builds a production version and uploads to Google Play Production | `bundle exec fastlane android deploy` |
|
||||
| `sync_version` | Syncs version from package.json to build.gradle | `bundle exec fastlane android sync_version` |
|
||||
|
||||
### Deployment Flow
|
||||
|
||||
1. **Version Management**: Update version in package.json using bump scripts
|
||||
2. **Build Process**: Run the appropriate lane for internal testing or production
|
||||
3. **Auto Build Numbers**: System automatically increments build numbers
|
||||
4. **Upload**: Artifacts are uploaded to respective app stores
|
||||
5. **Notification**: Slack notifications sent upon successful builds
|
||||
|
||||
## Local Development 💻
|
||||
|
||||
### Package Scripts
|
||||
|
||||
Several scripts in `app/package.json` facilitate common Fastlane and versioning tasks:
|
||||
|
||||
#### Debug Builds 🐞
|
||||
|
||||
**`yarn ios:fastlane-debug`** / **`yarn android:fastlane-debug`**
|
||||
|
||||
* Executes the `internal_test` Fastlane lane for the respective platforms
|
||||
* Builds the app in a debug configuration for internal testing
|
||||
* Uploads to TestFlight (iOS) or Google Play Internal Testing (Android) if permissions allow
|
||||
* Cleans build directories (`ios/build`, `android/app/build`) before running
|
||||
|
||||
#### Forced Local Deployment 🚀
|
||||
|
||||
**`yarn force-local-upload-deploy`**
|
||||
**`yarn force-local-upload-deploy:ios`**
|
||||
**`yarn force-local-upload-deploy:android`**
|
||||
|
||||
* Runs the `deploy` Fastlane lane with local development settings
|
||||
* Uses `FORCE_UPLOAD_LOCAL_DEV=true` to bypass CI checks
|
||||
* Useful for testing deployment process locally or manual deploys
|
||||
* Cleans build directories first
|
||||
* **Use with caution!** Will attempt to upload to production if you have permissions
|
||||
|
||||
#### Forced Local Testing 🧪
|
||||
|
||||
**`yarn force-local-upload-test`**
|
||||
**`yarn force-local-upload-test:ios`**
|
||||
**`yarn force-local-upload-test:android`**
|
||||
|
||||
* Similar to deploy version, but runs `internal_test` lane locally
|
||||
* Useful for testing the internal distribution process
|
||||
* Uses `FORCE_UPLOAD_LOCAL_DEV=true` flag
|
||||
|
||||
### Version Management 🏷️
|
||||
|
||||
**`yarn bump-version:major|minor|patch`**
|
||||
|
||||
* Increments version in `package.json` according to semantic versioning
|
||||
* Creates version commit and tag automatically
|
||||
* Calls `sync-versions` afterwards
|
||||
|
||||
**`yarn sync-versions`**
|
||||
|
||||
* Synchronizes the version from `package.json` to native files
|
||||
* Updates iOS `Info.plist` and Android `build.gradle`
|
||||
* Ensures consistency across JS bundle and native app wrappers
|
||||
|
||||
### Local Testing with `act` 🧰
|
||||
|
||||
You can test the GitHub Actions workflow locally using [`act`](https://github.com/nektos/act):
|
||||
|
||||
1. **Install `act`:** Follow the installation instructions in the `act` repository.
|
||||
|
||||
2. **Run Jobs:** From the *root* of the project repository:
|
||||
|
||||
```bash
|
||||
# Test the Android build
|
||||
act -j build-android --secret-file app/fastlane/.env.secrets
|
||||
|
||||
# Test the iOS build (limited functionality on non-macOS systems)
|
||||
act -j build-ios --secret-file app/fastlane/.env.secrets
|
||||
```
|
||||
|
||||
3. **Advanced Usage:**
|
||||
* When running with `act`, the environment variable `ACT=true` is set automatically
|
||||
* This causes certain steps to be skipped, like code signing and store uploads
|
||||
* You can modify the workflow file locally to focus on specific steps by adding `if: false` to steps you want to skip
|
||||
|
||||
4. **Limitations:**
|
||||
* iOS builds require macOS-specific tools not available in Docker
|
||||
* Certificate/provisioning profile handling may not work as expected
|
||||
* Network access to Apple/Google services may be limited
|
||||
|
||||
## CI/CD Pipeline 🔄
|
||||
|
||||
The primary CI/CD workflow is defined in `.github/workflows/mobile-deploy.yml`. It automates the build and deployment process.
|
||||
|
||||
### Triggers
|
||||
|
||||
* **Push Events:** Runs on pushes to `dev` or `main` branches that change files in `app/` or the workflow file
|
||||
* **Pull Request Events:** Runs on PRs to `dev` or `main` branches that change files in `app/` or the workflow file
|
||||
|
||||
### Jobs
|
||||
|
||||
The workflow consists of parallel jobs for each platform:
|
||||
|
||||
#### `build-ios` Job
|
||||
|
||||
Runs on `macos-latest` and performs the following steps:
|
||||
1. Sets up the environment (Node.js, Ruby, CocoaPods)
|
||||
2. Processes iOS secrets and certificates
|
||||
3. Runs appropriate Fastlane lane based on branch
|
||||
4. Commits updated build numbers back to the repository
|
||||
|
||||
#### `build-android` Job
|
||||
|
||||
Runs on `ubuntu-latest` and performs the following steps:
|
||||
1. Sets up the environment (Node.js, Java, Android SDK)
|
||||
2. Processes Android secrets
|
||||
3. Runs appropriate Fastlane lane based on branch
|
||||
4. Commits updated version code back to the repository
|
||||
|
||||
### Deployment Destinations
|
||||
|
||||
* **Internal Testing:**
|
||||
* iOS: TestFlight
|
||||
* Android: Google Play Internal Testing track
|
||||
* Triggered on pushes to `dev` branch and pull requests
|
||||
|
||||
* **Production:**
|
||||
* iOS: App Store Connect (ready for submission)
|
||||
* Android: Google Play Production track
|
||||
* Triggered on pushes to `main` branch
|
||||
|
||||
## Auto Build Number Incrementing 🔢
|
||||
|
||||
The CI/CD pipeline automatically manages build numbers/version codes:
|
||||
|
||||
### iOS Build Numbers
|
||||
|
||||
1. **Automatic Fetching:**
|
||||
* The pipeline fetches the latest build number from TestFlight via the App Store Connect API
|
||||
* Increments by 1 for the new build
|
||||
|
||||
2. **Implementation:**
|
||||
```ruby
|
||||
latest_build = Fastlane::Actions::LatestTestflightBuildNumberAction.run(
|
||||
api_key: api_key,
|
||||
app_identifier: ENV["IOS_APP_IDENTIFIER"],
|
||||
platform: "ios",
|
||||
)
|
||||
new_build_number = latest_build + 1
|
||||
```
|
||||
|
||||
3. **Commit Back to Repository:**
|
||||
* After incrementing, changes are automatically committed back to the branch
|
||||
* Files affected: `./app/ios/OpenPassport/Info.plist` and `./app/ios/Self.xcodeproj/project.pbxproj`
|
||||
|
||||
### Android Version Code
|
||||
|
||||
1. **Local Incrementing:**
|
||||
* The pipeline increments the version code in the Gradle file
|
||||
* Cannot verify against Google Play due to permission issues (see Android Caveats)
|
||||
|
||||
2. **Commit Back to Repository:**
|
||||
* After building, the workflow commits the incremented version code
|
||||
* File affected: `./app/android/app/build.gradle`
|
||||
|
||||
## Slack Notifications 💬
|
||||
|
||||
The CI/CD pipeline sends notifications to Slack after successful builds:
|
||||
|
||||
1. **Configuration:**
|
||||
* Set `SLACK_API_TOKEN` and `SLACK_ANNOUNCE_CHANNEL_NAME` in your `.env.secrets` file
|
||||
* For CI, add these as GitHub Actions Secrets
|
||||
|
||||
2. **Notification Content:**
|
||||
* iOS: `🍎 iOS v{version} (Build {build_number}) deployed to TestFlight/App Store Connect`
|
||||
* Android: `🤖 Android v{version} (Build {version_code}) deployed to Internal Testing/Google Play`
|
||||
* Includes the built artifact (IPA/AAB) as an attachment
|
||||
|
||||
3. **Testing Notifications:**
|
||||
* You can test Slack notifications locally with the `force-local-upload-test` scripts
|
||||
* Requires a valid Slack API token with proper permissions
|
||||
|
||||
## Platform-Specific Notes 📱
|
||||
|
||||
### Android Deployment Caveats ⚠️
|
||||
|
||||
There are important limitations when working with Android deployments:
|
||||
|
||||
1. **Google Play Store Permission Limitations:**
|
||||
* The pipeline currently **lacks permissions** to directly upload builds to the Google Play Store
|
||||
* The `android_has_permissions` flag in helpers.rb is set to false, preventing direct uploads
|
||||
|
||||
2. **Manual Upload Process Required:**
|
||||
* After the Android build job finishes, you must:
|
||||
1. Download the AAB artifact from the GitHub Actions run
|
||||
2. Manually upload the AAB file to the Google Play Console
|
||||
3. Complete the release process in the Play Console UI
|
||||
|
||||
3. **Version Code Management:**
|
||||
* Unlike iOS, we cannot automatically fetch the current Android build number (version code)
|
||||
* After building, you need to manually commit the updated version number
|
||||
|
||||
4. **For Local Developers:**
|
||||
* When testing Android deployment locally:
|
||||
```bash
|
||||
yarn android:build-release # Build the AAB
|
||||
# The AAB will be in android/app/build/outputs/bundle/release/app-release.aab
|
||||
```
|
||||
* Note that the `force-local-upload-deploy:android` script will attempt to deploy but will fail due to permission issues
|
||||
|
||||
## Troubleshooting 🔍
|
||||
|
||||
### Version Syncing Issues
|
||||
|
||||
If you encounter issues with version syncing between `package.json` and native projects:
|
||||
|
||||
1. **Manual Sync:**
|
||||
```bash
|
||||
yarn sync-versions
|
||||
```
|
||||
This runs the Fastlane lanes to sync versions without building or deploying.
|
||||
|
||||
2. **Version Mismatch Checking:**
|
||||
```bash
|
||||
# Check version in Info.plist
|
||||
/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" app/ios/OpenPassport/Info.plist
|
||||
|
||||
# Check version in build.gradle
|
||||
grep "versionName" app/android/app/build.gradle
|
||||
```
|
||||
|
||||
3. **Fixing Discrepancies:**
|
||||
* Always update `package.json` version first using the `bump-version` scripts
|
||||
* Then run `sync-versions` to update native files
|
||||
* For manual fixes, edit the version in each file and commit the changes
|
||||
|
||||
### iOS Build Issues
|
||||
|
||||
1. **Certificate/Provisioning Profile Errors**
|
||||
* Ensure your certificate and provisioning profile are valid and not expired
|
||||
* Verify that the correct team ID is being used
|
||||
* Try using `fastlane match` to manage certificates and profiles
|
||||
|
||||
2. **TestFlight Upload Failures**
|
||||
* Check that your App Store Connect API key has sufficient permissions
|
||||
* Verify your app's version and build numbers are incremented properly
|
||||
* Ensure binary is properly signed with distribution certificate
|
||||
|
||||
### Android Build Issues
|
||||
|
||||
1. **Keystore Issues**
|
||||
* Verify keystore path, password, and key alias are correct
|
||||
* Check file permissions on the keystore file
|
||||
* Ensure you're using the correct signing configuration in Gradle
|
||||
|
||||
2. **Google Play Upload Failures**
|
||||
* Verify the service account has proper permissions in the Google Play Console
|
||||
* Check that the app's version code has been incremented
|
||||
* Ensure the JSON key file is valid and not expired
|
||||
|
||||
## Additional Resources 📚
|
||||
|
||||
### Official Documentation
|
||||
|
||||
* [Fastlane Documentation](https://docs.fastlane.tools/)
|
||||
* [GitHub Actions Documentation](https://docs.github.com/en/actions)
|
||||
* [App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi)
|
||||
* [Google Play Developer API](https://developers.google.com/android-publisher)
|
||||
|
||||
### Helpful Tools
|
||||
|
||||
* [Match](https://docs.fastlane.tools/actions/match/) - Fastlane tool for iOS code signing
|
||||
* [Supply](https://docs.fastlane.tools/actions/supply/) - Fastlane tool for Android app deployment
|
||||
* [Gym](https://docs.fastlane.tools/actions/gym/) - Fastlane tool for building iOS apps
|
||||
293
app/fastlane/Fastfile
Normal file
293
app/fastlane/Fastfile
Normal file
@@ -0,0 +1,293 @@
|
||||
# This file contains the fastlane.tools configuration
|
||||
# You can find the documentation at https://docs.fastlane.tools
|
||||
#
|
||||
# For a list of all available actions, check out
|
||||
# https://docs.fastlane.tools/actions
|
||||
#
|
||||
|
||||
opt_out_usage
|
||||
|
||||
require "bundler/setup"
|
||||
require "base64"
|
||||
require_relative "helpers"
|
||||
|
||||
# load secrets before project configuration
|
||||
Fastlane::Helpers.dev_load_dotenv_secrets
|
||||
is_ci = Fastlane::Helpers.is_ci_environment?
|
||||
local_development = !is_ci
|
||||
|
||||
# checks after calling Dotenv.load
|
||||
attempt_force_upload_local_dev = ENV["FORCE_UPLOAD_LOCAL_DEV"] == "true"
|
||||
android_has_permissions = false
|
||||
|
||||
if local_development
|
||||
# confirm that we want to force upload
|
||||
Fastlane::Helpers.confirm_force_upload if attempt_force_upload_local_dev
|
||||
end
|
||||
|
||||
# Project configuration
|
||||
PROJECT_NAME = ENV["IOS_PROJECT_NAME"]
|
||||
PROJECT_SCHEME = ENV["IOS_PROJECT_SCHEME"]
|
||||
SIGNING_CERTIFICATE = ENV["IOS_SIGNING_CERTIFICATE"]
|
||||
|
||||
# Environment setup
|
||||
package_version = JSON.parse(File.read("../package.json"))["version"]
|
||||
# most of these values are for local development
|
||||
android_aab_path = "../android/app/build/outputs/bundle/release/app-release.aab"
|
||||
android_gradle_file_path = "../android/app/build.gradle"
|
||||
android_keystore_path = "../android/app/upload-keystore.jks"
|
||||
android_play_store_json_key_path = "../android/app/play-store-key.json"
|
||||
ios_connect_api_key_path = "../ios/certs/connect_api_key.p8"
|
||||
ios_provisioning_profile_directory = "~/Library/MobileDevice/Provisioning\ Profiles"
|
||||
ios_xcode_profile_path = "../ios/#{PROJECT_NAME}.xcodeproj"
|
||||
|
||||
default_platform(:ios)
|
||||
|
||||
platform :ios do
|
||||
desc "Sync ios version"
|
||||
lane :sync_version do
|
||||
increment_version_number(
|
||||
xcodeproj: "ios/#{PROJECT_NAME}.xcodeproj",
|
||||
version_number: package_version,
|
||||
)
|
||||
end
|
||||
|
||||
desc "Push a new build to TestFlight Internal Testing"
|
||||
lane :internal_test do
|
||||
result = prepare_ios_build(prod_release: false)
|
||||
|
||||
upload_to_testflight(
|
||||
api_key: result[:api_key],
|
||||
distribute_external: true,
|
||||
groups: ENV["IOS_TESTFLIGHT_GROUPS"].split(","),
|
||||
changelog: "",
|
||||
skip_waiting_for_build_processing: false,
|
||||
) if result[:should_upload]
|
||||
|
||||
# Notify Slack about the new build
|
||||
if ENV["SLACK_CHANNEL_ID"]
|
||||
Fastlane::Helpers.upload_file_to_slack(
|
||||
file_path: result[:ipa_path],
|
||||
channel_id: ENV["SLACK_CHANNEL_ID"],
|
||||
initial_comment: "🍎 iOS v#{package_version} (Build #{result[:build_number]}) deployed to TestFlight",
|
||||
title: "#{PROJECT_NAME}-#{package_version}-#{result[:build_number]}.ipa",
|
||||
)
|
||||
else
|
||||
UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.")
|
||||
end
|
||||
end
|
||||
|
||||
desc "Prepare a new build for App Store submission"
|
||||
lane :deploy do
|
||||
result = prepare_ios_build(prod_release: true)
|
||||
|
||||
upload_to_app_store(
|
||||
api_key: result[:api_key],
|
||||
skip_screenshots: true,
|
||||
skip_metadata: true,
|
||||
submit_for_review: false,
|
||||
automatic_release: false,
|
||||
skip_app_version_update: true,
|
||||
) if result[:should_upload]
|
||||
|
||||
# Notify Slack about the new build
|
||||
if ENV["SLACK_CHANNEL_ID"]
|
||||
Fastlane::Helpers.upload_file_to_slack(
|
||||
file_path: result[:ipa_path],
|
||||
channel_id: ENV["SLACK_CHANNEL_ID"],
|
||||
initial_comment: "🍎 iOS (Ready for Submission) v#{package_version} (Build #{result[:build_number]}) deployed to App Store Connect",
|
||||
title: "#{PROJECT_NAME}-#{package_version}-#{result[:build_number]}.ipa",
|
||||
)
|
||||
else
|
||||
UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.")
|
||||
end
|
||||
end
|
||||
|
||||
private_lane :prepare_ios_build do |options|
|
||||
if local_development
|
||||
# app breaks with Xcode 16.3
|
||||
xcode_select "/Applications/Xcode-16-2.app"
|
||||
|
||||
# Set up API key, profile, and potentially certificate for local dev
|
||||
Fastlane::Helpers.ios_dev_setup_connect_api_key(ios_connect_api_key_path)
|
||||
Fastlane::Helpers.ios_dev_setup_provisioning_profile(ios_provisioning_profile_directory)
|
||||
Fastlane::Helpers.ios_dev_setup_certificate
|
||||
else
|
||||
# we need this for building ios apps in CI
|
||||
# else build will hang on "[CP] Embed Pods Frameworks"
|
||||
setup_ci(
|
||||
keychain_name: "build.keychain",
|
||||
)
|
||||
end
|
||||
|
||||
required_env_vars = [
|
||||
"IOS_APP_IDENTIFIER",
|
||||
"IOS_CONNECT_API_KEY_BASE64",
|
||||
"IOS_CONNECT_API_KEY_PATH",
|
||||
"IOS_CONNECT_ISSUER_ID",
|
||||
"IOS_CONNECT_KEY_ID",
|
||||
"IOS_PROJECT_NAME",
|
||||
"IOS_PROJECT_SCHEME",
|
||||
"IOS_PROV_PROFILE_NAME",
|
||||
"IOS_PROV_PROFILE_PATH",
|
||||
"IOS_TEAM_ID",
|
||||
"IOS_TEAM_NAME",
|
||||
]
|
||||
|
||||
target_platform = options[:prod_release] ? "App Store" : "TestFlight"
|
||||
should_upload = Fastlane::Helpers.should_upload_app(target_platform)
|
||||
workspace_path = File.expand_path("../ios/#{PROJECT_NAME}.xcworkspace", Dir.pwd)
|
||||
ios_signing_certificate_name = "iPhone Distribution: #{ENV["IOS_TEAM_NAME"]} (#{ENV["IOS_TEAM_ID"]})"
|
||||
|
||||
Fastlane::Helpers.verify_env_vars(required_env_vars)
|
||||
build_number = Fastlane::Helpers.ios_increment_build_number(ios_xcode_profile_path)
|
||||
Fastlane::Helpers.ios_verify_app_store_build_number(ios_xcode_profile_path)
|
||||
Fastlane::Helpers.ios_verify_provisioning_profile
|
||||
|
||||
api_key = app_store_connect_api_key(
|
||||
key_id: ENV["IOS_CONNECT_KEY_ID"],
|
||||
issuer_id: ENV["IOS_CONNECT_ISSUER_ID"],
|
||||
key_filepath: ENV["IOS_CONNECT_API_KEY_PATH"],
|
||||
in_house: false,
|
||||
)
|
||||
|
||||
# Update project to use manual code signing
|
||||
update_code_signing_settings(
|
||||
use_automatic_signing: false,
|
||||
path: "ios/#{PROJECT_NAME}.xcodeproj",
|
||||
team_id: ENV["IOS_TEAM_ID"],
|
||||
targets: [PROJECT_NAME],
|
||||
code_sign_identity: ios_signing_certificate_name,
|
||||
profile_name: ENV["IOS_PROV_PROFILE_NAME"],
|
||||
bundle_identifier: ENV["IOS_APP_IDENTIFIER"],
|
||||
build_configurations: ["Release"],
|
||||
)
|
||||
|
||||
clear_derived_data
|
||||
|
||||
# Print final build settings before archiving
|
||||
sh "xcodebuild -showBuildSettings -workspace #{workspace_path} " \
|
||||
"-scheme #{PROJECT_SCHEME} -configuration Release " \
|
||||
"| grep 'CODE_SIGN_STYLE\|PROVISIONING_PROFILE_SPECIFIER\|CODE_SIGN_IDENTITY\|DEVELOPMENT_TEAM' || true"
|
||||
|
||||
cocoapods(
|
||||
podfile: "ios/Podfile",
|
||||
clean_install: true,
|
||||
deployment: true,
|
||||
)
|
||||
|
||||
ipa_path = build_app({
|
||||
workspace: "#{workspace_path}",
|
||||
scheme: PROJECT_SCHEME,
|
||||
export_method: "app-store",
|
||||
output_directory: "build",
|
||||
clean: true,
|
||||
export_options: {
|
||||
method: "app-store",
|
||||
signingStyle: "manual",
|
||||
provisioningProfiles: {
|
||||
ENV["IOS_APP_IDENTIFIER"] => ENV["IOS_PROV_PROFILE_NAME"],
|
||||
},
|
||||
signingCertificate: ios_signing_certificate_name,
|
||||
teamID: ENV["IOS_TEAM_ID"],
|
||||
},
|
||||
})
|
||||
|
||||
{
|
||||
api_key: api_key,
|
||||
build_number: build_number,
|
||||
ipa_path: ipa_path,
|
||||
should_upload: should_upload,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
platform :android do
|
||||
desc "Sync android version"
|
||||
lane :sync_version do
|
||||
android_set_version_name(
|
||||
version_name: package_version,
|
||||
gradle_file: android_gradle_file_path,
|
||||
)
|
||||
end
|
||||
|
||||
desc "Push a new build to Google Play Internal Testing"
|
||||
lane :internal_test do
|
||||
upload_android_build(track: "internal")
|
||||
end
|
||||
|
||||
desc "Push a new build to Google Play Store"
|
||||
lane :deploy do
|
||||
upload_android_build(track: "production")
|
||||
end
|
||||
|
||||
private_lane :upload_android_build do |options|
|
||||
if local_development
|
||||
if ENV["ANDROID_KEYSTORE_PATH"].nil?
|
||||
ENV["ANDROID_KEYSTORE_PATH"] = Fastlane::Helpers.android_create_keystore(android_keystore_path)
|
||||
end
|
||||
|
||||
if ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"].nil?
|
||||
ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"] = Fastlane::Helpers.android_create_play_store_key(android_play_store_json_key_path)
|
||||
end
|
||||
end
|
||||
|
||||
required_env_vars = [
|
||||
"ANDROID_KEYSTORE",
|
||||
"ANDROID_KEYSTORE_PASSWORD",
|
||||
"ANDROID_KEYSTORE_PATH",
|
||||
"ANDROID_KEY_ALIAS",
|
||||
"ANDROID_KEY_PASSWORD",
|
||||
"ANDROID_PACKAGE_NAME",
|
||||
"ANDROID_PLAY_STORE_JSON_KEY_PATH",
|
||||
]
|
||||
|
||||
Fastlane::Helpers.verify_env_vars(required_env_vars)
|
||||
version_code = Fastlane::Helpers.android_increment_version_code(android_gradle_file_path)
|
||||
# TODO: uncomment when we have the permissions to run this action
|
||||
# Fastlane::Helpers.android_verify_version_code(android_gradle_file_path)
|
||||
|
||||
target_platform = options[:track] == "production" ? "Google Play" : "Internal Testing"
|
||||
should_upload = Fastlane::Helpers.should_upload_app(target_platform)
|
||||
|
||||
validate_play_store_json_key(
|
||||
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
|
||||
)
|
||||
|
||||
Fastlane::Helpers.with_retry(max_retries: 3, delay: 10) do
|
||||
gradle(
|
||||
task: "clean bundleRelease",
|
||||
project_dir: "android/",
|
||||
properties: {
|
||||
"android.injected.signing.store.file" => ENV["ANDROID_KEYSTORE_PATH"],
|
||||
"android.injected.signing.store.password" => ENV["ANDROID_KEYSTORE_PASSWORD"],
|
||||
"android.injected.signing.key.alias" => ENV["ANDROID_KEY_ALIAS"],
|
||||
"android.injected.signing.key.password" => ENV["ANDROID_KEY_PASSWORD"],
|
||||
},
|
||||
)
|
||||
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
|
||||
|
||||
# Notify Slack about the new build
|
||||
if ENV["SLACK_CHANNEL_ID"]
|
||||
Fastlane::Helpers.upload_file_to_slack(
|
||||
file_path: android_aab_path,
|
||||
channel_id: ENV["SLACK_CHANNEL_ID"],
|
||||
initial_comment: "🤖 Android v#{package_version} (Build #{version_code}) deployed to #{target_platform}",
|
||||
title: "#{PROJECT_NAME}-#{package_version}-#{version_code}.aab",
|
||||
)
|
||||
else
|
||||
UI.important("Skipping Slack notification: SLACK_CHANNEL_ID not set.")
|
||||
end
|
||||
end
|
||||
end
|
||||
6
app/fastlane/Pluginfile
Normal file
6
app/fastlane/Pluginfile
Normal file
@@ -0,0 +1,6 @@
|
||||
# Autogenerated by fastlane
|
||||
#
|
||||
# Ensure this file is checked in to source control!
|
||||
|
||||
gem 'fastlane-plugin-increment_version_code'
|
||||
gem 'fastlane-plugin-versioning_android'
|
||||
77
app/fastlane/README.md
Normal file
77
app/fastlane/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
fastlane documentation
|
||||
----
|
||||
|
||||
# Installation
|
||||
|
||||
Make sure you have the latest version of the Xcode command line tools installed:
|
||||
|
||||
```sh
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||
|
||||
# Available Actions
|
||||
|
||||
## iOS
|
||||
|
||||
### ios sync_version
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios sync_version
|
||||
```
|
||||
|
||||
Sync ios version
|
||||
|
||||
### ios internal_test
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios internal_test
|
||||
```
|
||||
|
||||
Push a new build to TestFlight Internal Testing
|
||||
|
||||
### ios deploy
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios deploy
|
||||
```
|
||||
|
||||
Prepare a new build for App Store submission
|
||||
|
||||
----
|
||||
|
||||
|
||||
## Android
|
||||
|
||||
### android sync_version
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android sync_version
|
||||
```
|
||||
|
||||
Sync android version
|
||||
|
||||
### android internal_test
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android internal_test
|
||||
```
|
||||
|
||||
Push a new build to Google Play Internal Testing
|
||||
|
||||
### android deploy
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android deploy
|
||||
```
|
||||
|
||||
Push a new build to Google Play Store
|
||||
|
||||
----
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||
|
||||
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||
|
||||
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||
739
app/fastlane/helpers.rb
Normal file
739
app/fastlane/helpers.rb
Normal file
@@ -0,0 +1,739 @@
|
||||
require "bundler/setup"
|
||||
require "fastlane"
|
||||
require "tempfile"
|
||||
require "fileutils"
|
||||
require "base64"
|
||||
require "shellwords"
|
||||
require "net/http"
|
||||
require "uri"
|
||||
require "json"
|
||||
|
||||
# Load secrets before defining constants
|
||||
module Fastlane
|
||||
module Helpers
|
||||
def self.is_ci_environment?
|
||||
ENV["CI"] == "true" && ENV["ACT"] != "true"
|
||||
end
|
||||
|
||||
def self.dev_load_dotenv_secrets
|
||||
if !is_ci_environment?
|
||||
puts "Loading .env.secrets"
|
||||
require "dotenv"
|
||||
Dotenv.load("./.env.secrets")
|
||||
end
|
||||
end
|
||||
|
||||
# Simple multipart boundary generator
|
||||
def self.generate_boundary
|
||||
"----FastlaneSlackUploadBoundary#{rand(1000000)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Call load_dotenv_secrets before setting constants
|
||||
Fastlane::Helpers.dev_load_dotenv_secrets
|
||||
|
||||
# Now set constants after secrets are loaded
|
||||
SLACK_TOKEN = ENV["SLACK_API_TOKEN"]
|
||||
CHANNEL_NAME = ENV["SLACK_ANNOUNCE_CHANNEL_NAME"] || "deploy-mobile"
|
||||
|
||||
module Fastlane
|
||||
module Helpers
|
||||
@@android_has_permissions = false
|
||||
|
||||
### UI and Reporting Methods ###
|
||||
def self.report_error(message, suggestion = nil, abort_message = nil)
|
||||
UI.error("❌ #{message}")
|
||||
UI.error(suggestion) if suggestion
|
||||
UI.abort_with_message!(abort_message || message)
|
||||
end
|
||||
|
||||
def self.report_success(message)
|
||||
UI.success("✅ #{message}")
|
||||
end
|
||||
|
||||
### Environment and Configuration Methods ###
|
||||
def self.verify_env_vars(required_vars)
|
||||
missing_vars = required_vars.select { |var| ENV[var].nil? || ENV[var].to_s.strip.empty? }
|
||||
|
||||
if missing_vars.any?
|
||||
report_error(
|
||||
"Missing required environment variables: #{missing_vars.join(", ")}",
|
||||
"Please check your secrets",
|
||||
"Environment verification failed"
|
||||
)
|
||||
else
|
||||
report_success("All required environment variables are present")
|
||||
end
|
||||
end
|
||||
|
||||
def self.should_upload_app(platform)
|
||||
if ENV["ACT"] == "true"
|
||||
puts "Skipping upload to #{platform} we are testing using `act`"
|
||||
return false
|
||||
end
|
||||
|
||||
if ENV["IS_PR"] == "true"
|
||||
puts "Skipping upload to #{platform} because we are in a pull request"
|
||||
return false
|
||||
end
|
||||
|
||||
# upload app if we are in CI or forcing local upload
|
||||
ENV["CI"] == "true" || ENV["FORCE_UPLOAD_LOCAL_DEV"] == "true"
|
||||
end
|
||||
|
||||
def self.confirm_force_upload
|
||||
UI.important "⚠️ FORCE_UPLOAD_LOCAL_DEV is set to true. This will upload the build to the store."
|
||||
UI.important "Are you sure you want to continue? (y/n)"
|
||||
response = STDIN.gets.chomp
|
||||
unless response.downcase == "y"
|
||||
UI.user_error!("Upload cancelled by user")
|
||||
end
|
||||
end
|
||||
|
||||
def self.with_retry(max_retries: 3, delay: 5)
|
||||
attempts = 0
|
||||
begin
|
||||
yield
|
||||
rescue => e
|
||||
attempts += 1
|
||||
if attempts < max_retries
|
||||
UI.important("Retry ##{attempts} after error: #{e.message}")
|
||||
sleep(delay)
|
||||
retry
|
||||
else
|
||||
UI.user_error!("Failed after #{max_retries} retries: #{e.message}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.ios_verify_app_store_build_number(ios_xcode_profile_path)
|
||||
api_key = Fastlane::Actions::AppStoreConnectApiKeyAction.run(
|
||||
key_id: ENV["IOS_CONNECT_KEY_ID"],
|
||||
issuer_id: ENV["IOS_CONNECT_ISSUER_ID"],
|
||||
key_filepath: ENV["IOS_CONNECT_API_KEY_PATH"],
|
||||
in_house: false,
|
||||
)
|
||||
|
||||
latest_build = Fastlane::Actions::LatestTestflightBuildNumberAction.run(
|
||||
api_key: api_key,
|
||||
app_identifier: ENV["IOS_APP_IDENTIFIER"],
|
||||
platform: "ios",
|
||||
)
|
||||
|
||||
project = Xcodeproj::Project.open(ios_xcode_profile_path)
|
||||
target = project.targets.first
|
||||
current_build = target.build_configurations.first.build_settings["CURRENT_PROJECT_VERSION"]
|
||||
|
||||
if current_build.to_i <= latest_build.to_i
|
||||
report_error(
|
||||
"Build number must be greater than latest TestFlight build!",
|
||||
"Latest TestFlight build: #{latest_build}\nCurrent build: #{current_build}\nPlease increment the build number in the project settings",
|
||||
"Build number verification failed"
|
||||
)
|
||||
else
|
||||
report_success("Build number verified (Current: #{current_build}, Latest TestFlight: #{latest_build})")
|
||||
end
|
||||
end
|
||||
|
||||
def self.ios_ensure_generic_versioning(ios_xcode_profile_path)
|
||||
puts "Opening Xcode project at: #{File.expand_path(ios_xcode_profile_path)}"
|
||||
|
||||
unless File.exist?(ios_xcode_profile_path)
|
||||
report_error(
|
||||
"Xcode project not found at #{project_path}",
|
||||
"Please ensure you're running this command from the correct directory",
|
||||
"Project file not found"
|
||||
)
|
||||
end
|
||||
|
||||
project = Xcodeproj::Project.open(ios_xcode_profile_path)
|
||||
|
||||
project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
if config.build_settings["VERSIONING_SYSTEM"] != "apple-generic"
|
||||
puts "Enabling Apple Generic Versioning for #{target.name} - #{config.name}"
|
||||
config.build_settings["VERSIONING_SYSTEM"] = "apple-generic"
|
||||
config.build_settings["CURRENT_PROJECT_VERSION"] ||= "1"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
project.save
|
||||
report_success("Enabled Apple Generic Versioning in Xcode project")
|
||||
end
|
||||
|
||||
def self.ios_increment_build_number(ios_xcode_profile_path)
|
||||
# First ensure Apple Generic Versioning is enabled
|
||||
ios_ensure_generic_versioning(ios_xcode_profile_path)
|
||||
|
||||
api_key = Fastlane::Actions::AppStoreConnectApiKeyAction.run(
|
||||
key_id: ENV["IOS_CONNECT_KEY_ID"],
|
||||
issuer_id: ENV["IOS_CONNECT_ISSUER_ID"],
|
||||
key_filepath: ENV["IOS_CONNECT_API_KEY_PATH"],
|
||||
in_house: false,
|
||||
)
|
||||
|
||||
latest_build = Fastlane::Actions::LatestTestflightBuildNumberAction.run(
|
||||
api_key: api_key,
|
||||
app_identifier: ENV["IOS_APP_IDENTIFIER"],
|
||||
platform: "ios",
|
||||
)
|
||||
|
||||
new_build_number = latest_build + 1
|
||||
|
||||
Fastlane::Actions::IncrementBuildNumberAction.run(
|
||||
build_number: new_build_number,
|
||||
xcodeproj: ios_xcode_profile_path,
|
||||
)
|
||||
|
||||
report_success("Incremented build number to #{new_build_number} (previous TestFlight build: #{latest_build})")
|
||||
|
||||
new_build_number
|
||||
end
|
||||
|
||||
def self.ios_dev_setup_certificate
|
||||
unless ENV["IOS_DIST_CERT_BASE64"]
|
||||
report_error(
|
||||
"Missing IOS_DIST_CERT_BASE64 environment variable.",
|
||||
"This variable is required for local certificate installation.",
|
||||
"Certificate installation failed"
|
||||
)
|
||||
end
|
||||
unless ENV["IOS_P12_PASSWORD"]
|
||||
report_error(
|
||||
"Missing IOS_P12_PASSWORD environment variable.",
|
||||
"This password is required to import the certificate (.p12 file).",
|
||||
"Certificate installation failed"
|
||||
)
|
||||
end
|
||||
|
||||
decoded_cert_data = Base64.decode64(ENV["IOS_DIST_CERT_BASE64"])
|
||||
if decoded_cert_data.empty?
|
||||
report_error(
|
||||
"IOS_DIST_CERT_BASE64 seems to be empty or invalid.",
|
||||
"Please check the value of the environment variable.",
|
||||
"Certificate decoding failed"
|
||||
)
|
||||
end
|
||||
|
||||
cert_password = ENV["IOS_P12_PASSWORD"] || ""
|
||||
temp_p12 = nil
|
||||
|
||||
begin
|
||||
temp_p12 = Tempfile.new(["fastlane_local_cert", ".p12"])
|
||||
temp_p12.binmode
|
||||
temp_p12.write(decoded_cert_data)
|
||||
temp_p12.close
|
||||
puts "Temporarily wrote decoded certificate to: #{temp_p12.path}"
|
||||
|
||||
# Import the certificate into the default keychain
|
||||
# Omitting -k targets the default keychain.
|
||||
# -T /usr/bin/codesign allows codesign to use the key without prompting every time.
|
||||
import_command = "security import #{Shellwords.escape(temp_p12.path)} -P #{Shellwords.escape(cert_password)} -T /usr/bin/codesign"
|
||||
puts "Running: #{import_command}"
|
||||
import_output = `#{import_command} 2>&1`
|
||||
|
||||
unless $?.success?
|
||||
report_error(
|
||||
"Failed to import certificate into default keychain.",
|
||||
"Command: #{import_command}\nOutput: #{import_output}",
|
||||
"Certificate import failed"
|
||||
)
|
||||
end
|
||||
report_success("Certificate imported successfully into default keychain.")
|
||||
rescue => e
|
||||
report_error("An error occurred during certificate installation: #{e.message}", e.backtrace.join("\n"), "Certificate installation failed")
|
||||
ensure
|
||||
# Clean up temporary file
|
||||
if temp_p12
|
||||
temp_p12.unlink
|
||||
puts "Cleaned up temp certificate: #{temp_p12.path}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.ios_dev_setup_connect_api_key(api_key_path)
|
||||
api_key_full_path = File.expand_path(api_key_path, File.dirname(__FILE__))
|
||||
ENV["IOS_CONNECT_API_KEY_PATH"] = api_key_full_path
|
||||
|
||||
if ENV["IOS_CONNECT_API_KEY_BASE64"]
|
||||
puts "Decoding iOS Connect API key..."
|
||||
begin
|
||||
decoded_key = Base64.decode64(ENV["IOS_CONNECT_API_KEY_BASE64"])
|
||||
if decoded_key.empty?
|
||||
report_error(
|
||||
"IOS_CONNECT_API_KEY_BASE64 seems to be empty or invalid.",
|
||||
"Please check the value of the environment variable.",
|
||||
"Connect API Key decoding failed"
|
||||
)
|
||||
end
|
||||
FileUtils.mkdir_p(File.dirname(api_key_full_path))
|
||||
File.write(api_key_full_path, decoded_key)
|
||||
report_success("Connect API Key written to: #{api_key_full_path}")
|
||||
rescue => e
|
||||
report_error("Error writing decoded API key: #{e.message}", nil, "Connect API Key setup failed")
|
||||
end
|
||||
elsif !File.exist?(api_key_full_path)
|
||||
report_error(
|
||||
"IOS_CONNECT_API_KEY_BASE64 not set and key file not found.",
|
||||
"Please provide the key via environment variable or ensure it exists at #{api_key_full_path}",
|
||||
"Connect API Key setup failed"
|
||||
)
|
||||
else
|
||||
puts "Using existing Connect API Key at: #{api_key_full_path}"
|
||||
end
|
||||
|
||||
begin
|
||||
verified_path = File.realpath(api_key_full_path)
|
||||
puts "Verified Connect API Key path: #{verified_path}"
|
||||
verified_path
|
||||
rescue Errno::ENOENT
|
||||
report_error("Connect API Key file not found at expected location: #{api_key_full_path}", nil, "Connect API Key verification failed")
|
||||
end
|
||||
end
|
||||
|
||||
def self.ios_dev_setup_provisioning_profile(provisioning_profile_directory)
|
||||
unless ENV["IOS_PROV_PROFILE_BASE64"]
|
||||
report_error(
|
||||
"Missing IOS_PROV_PROFILE_BASE64 environment variable.",
|
||||
"This variable is required for local development profile setup.",
|
||||
"Provisioning profile setup failed"
|
||||
)
|
||||
end
|
||||
|
||||
decoded_profile_data = Base64.decode64(ENV["IOS_PROV_PROFILE_BASE64"])
|
||||
if decoded_profile_data.empty?
|
||||
report_error(
|
||||
"IOS_PROV_PROFILE_BASE64 seems to be empty or invalid.",
|
||||
"Please check the value of the environment variable.",
|
||||
"Provisioning profile decoding failed"
|
||||
)
|
||||
end
|
||||
|
||||
temp_profile = nil
|
||||
temp_plist = nil
|
||||
final_path = nil
|
||||
|
||||
begin
|
||||
temp_profile = Tempfile.new(["fastlane_local_profile", ".mobileprovision"])
|
||||
temp_profile.binmode
|
||||
temp_profile.write(decoded_profile_data)
|
||||
temp_profile.close
|
||||
puts "Temporarily wrote decoded profile to: #{temp_profile.path}"
|
||||
|
||||
temp_plist = Tempfile.new(["fastlane_temp_plist", ".plist"])
|
||||
temp_plist_path = temp_plist.path
|
||||
temp_plist.close
|
||||
puts "Temporary plist path: #{temp_plist_path}"
|
||||
|
||||
security_command = "security cms -D -i #{Shellwords.escape(temp_profile.path)} -o #{Shellwords.escape(temp_plist_path)}"
|
||||
puts "Running: #{security_command}"
|
||||
security_output = `#{security_command} 2>&1`
|
||||
|
||||
unless $?.success?
|
||||
report_error(
|
||||
"Failed to extract plist from provisioning profile using security cms.",
|
||||
"Command failed: #{security_command}\nOutput: #{security_output}",
|
||||
"Provisioning profile UUID extraction failed"
|
||||
)
|
||||
end
|
||||
puts "Successfully extracted plist."
|
||||
|
||||
unless File.exist?(temp_plist_path) && File.size(temp_plist_path) > 0
|
||||
report_error(
|
||||
"Plist file was not created or is empty after security command.",
|
||||
"Expected plist at: #{temp_plist_path}",
|
||||
"Provisioning profile UUID extraction failed"
|
||||
)
|
||||
end
|
||||
|
||||
plistbuddy_command = "/usr/libexec/PlistBuddy -c \"Print :UUID\" #{Shellwords.escape(temp_plist_path)}"
|
||||
puts "Running: #{plistbuddy_command}"
|
||||
profile_uuid = `#{plistbuddy_command} 2>&1`.strip
|
||||
|
||||
unless $?.success? && !profile_uuid.empty? && profile_uuid !~ /does not exist/
|
||||
report_error(
|
||||
"Failed to extract UUID using PlistBuddy or UUID was empty.",
|
||||
"Command: #{plistbuddy_command}\nOutput: #{profile_uuid}",
|
||||
"Provisioning profile UUID extraction failed"
|
||||
)
|
||||
end
|
||||
report_success("Extracted profile UUID: #{profile_uuid}")
|
||||
|
||||
profile_dir = File.expand_path(provisioning_profile_directory)
|
||||
FileUtils.mkdir_p(profile_dir)
|
||||
final_path = File.join(profile_dir, "#{profile_uuid}.mobileprovision")
|
||||
|
||||
puts "Copying profile to: #{final_path}"
|
||||
FileUtils.cp(temp_profile.path, final_path)
|
||||
report_success("Provisioning profile installed successfully.")
|
||||
|
||||
ENV["IOS_PROV_PROFILE_PATH"] = final_path
|
||||
rescue => e
|
||||
report_error("An error occurred during provisioning profile setup: #{e.message}", e.backtrace.join("\n"), "Provisioning profile setup failed")
|
||||
ensure
|
||||
if temp_profile
|
||||
temp_profile.unlink
|
||||
puts "Cleaned up temp profile: #{temp_profile.path}"
|
||||
end
|
||||
if temp_plist_path && File.exist?(temp_plist_path)
|
||||
File.unlink(temp_plist_path)
|
||||
puts "Cleaned up temp plist: #{temp_plist_path}"
|
||||
end
|
||||
end
|
||||
|
||||
final_path
|
||||
end
|
||||
|
||||
def self.ios_verify_provisioning_profile
|
||||
profile_path = ENV["IOS_PROV_PROFILE_PATH"]
|
||||
|
||||
unless profile_path && !profile_path.empty?
|
||||
report_error(
|
||||
"ENV['IOS_PROV_PROFILE_PATH'] is not set.",
|
||||
"Ensure ios_dev_setup_provisioning_profile ran successfully or the path is set correctly in CI.",
|
||||
"Provisioning profile verification failed"
|
||||
)
|
||||
end
|
||||
|
||||
puts "Verifying provisioning profile exists at: #{profile_path}"
|
||||
|
||||
begin
|
||||
File.realpath(profile_path)
|
||||
report_success("iOS provisioning profile verified successfully at #{profile_path}")
|
||||
rescue Errno::ENOENT
|
||||
report_error("Provisioning profile not found at: #{profile_path}")
|
||||
rescue => e
|
||||
report_error("Error accessing provisioning profile at #{profile_path}: #{e.message}")
|
||||
end
|
||||
|
||||
# Print current user
|
||||
current_user = ENV["USER"] || `whoami`.strip
|
||||
puts "Current user: #{current_user}"
|
||||
|
||||
# List all provisioning profiles in user's directory
|
||||
profiles_dir = File.expand_path("~/Library/MobileDevice/Provisioning Profiles")
|
||||
if Dir.exist?(profiles_dir)
|
||||
puts "Listing mobile provisioning profiles in #{profiles_dir}:"
|
||||
profiles = Dir.glob(File.join(profiles_dir, "*.mobileprovision"))
|
||||
if profiles.empty?
|
||||
puts " No provisioning profiles found"
|
||||
else
|
||||
profiles.each do |profile|
|
||||
uuid = File.basename(profile, ".mobileprovision")
|
||||
puts " - #{uuid}.mobileprovision"
|
||||
end
|
||||
puts "Total provisioning profiles found: #{profiles.count}"
|
||||
end
|
||||
else
|
||||
puts "Provisioning profiles directory not found at: #{profiles_dir}"
|
||||
end
|
||||
|
||||
# Advanced checks for provisioning profile
|
||||
puts "\n--- Advanced Provisioning Profile Diagnostics ---"
|
||||
|
||||
# Check if profile can be parsed
|
||||
if File.exist?(profile_path)
|
||||
puts "Testing if profile can be parsed with security tool:"
|
||||
temp_plist = Tempfile.new(["profile_info", ".plist"])
|
||||
begin
|
||||
security_cmd = "security cms -D -i #{Shellwords.escape(profile_path)} -o #{Shellwords.escape(temp_plist.path)}"
|
||||
security_output = `#{security_cmd} 2>&1`
|
||||
security_success = $?.success?
|
||||
|
||||
if security_success
|
||||
puts "✅ Profile can be parsed successfully"
|
||||
|
||||
# Extract and display important profile information
|
||||
puts "\nExtracting profile information:"
|
||||
|
||||
# Get profile UUID
|
||||
uuid_cmd = "/usr/libexec/PlistBuddy -c 'Print :UUID' #{Shellwords.escape(temp_plist.path)}"
|
||||
uuid = `#{uuid_cmd}`.strip
|
||||
puts "Profile UUID: #{uuid}"
|
||||
|
||||
# Get App ID/Bundle ID
|
||||
app_id_cmd = "/usr/libexec/PlistBuddy -c 'Print :Entitlements:application-identifier' #{Shellwords.escape(temp_plist.path)}"
|
||||
app_id = `#{app_id_cmd}`.strip
|
||||
puts "App Identifier: #{app_id}"
|
||||
|
||||
# Get Team ID
|
||||
team_id_cmd = "/usr/libexec/PlistBuddy -c 'Print :TeamIdentifier:0' #{Shellwords.escape(temp_plist.path)}"
|
||||
team_id = `#{team_id_cmd}`.strip
|
||||
puts "Team Identifier: #{team_id}"
|
||||
|
||||
# Get profile type (development, distribution, etc.)
|
||||
profile_type_cmd = "/usr/libexec/PlistBuddy -c 'Print :Entitlements:get-task-allow' #{Shellwords.escape(temp_plist.path)} 2>/dev/null"
|
||||
get_task_allow = `#{profile_type_cmd}`.strip.downcase
|
||||
|
||||
if get_task_allow == "true"
|
||||
puts "Profile Type: Development"
|
||||
else
|
||||
distribution_cmd = "/usr/libexec/PlistBuddy -c 'Print :ProvisionsAllDevices' #{Shellwords.escape(temp_plist.path)} 2>/dev/null"
|
||||
provisions_all = `#{distribution_cmd}`.strip.downcase
|
||||
|
||||
if provisions_all == "true"
|
||||
puts "Profile Type: Enterprise Distribution"
|
||||
else
|
||||
puts "Profile Type: App Store Distribution"
|
||||
end
|
||||
end
|
||||
|
||||
# Get expiration date
|
||||
expiration_cmd = "/usr/libexec/PlistBuddy -c 'Print :ExpirationDate' #{Shellwords.escape(temp_plist.path)}"
|
||||
expiration = `#{expiration_cmd}`.strip
|
||||
puts "Expiration Date: #{expiration}"
|
||||
else
|
||||
puts "❌ Failed to parse profile: #{security_output}"
|
||||
end
|
||||
ensure
|
||||
temp_plist.close
|
||||
temp_plist.unlink
|
||||
end
|
||||
end
|
||||
|
||||
# Check code signing identities
|
||||
puts "\nInspecting code signing identities:"
|
||||
signing_identities = `security find-identity -v -p codesigning 2>&1`
|
||||
puts signing_identities
|
||||
|
||||
# Check keychain configuration
|
||||
puts "\nKeychain configuration:"
|
||||
puts `security list-keychains -d user 2>&1`
|
||||
|
||||
# Check Xcode configuration
|
||||
puts "\nXcode code signing search paths:"
|
||||
puts "Provisioning profiles search path: ~/Library/MobileDevice/Provisioning Profiles/"
|
||||
puts "Recommended check: In Xcode settings, verify your Apple ID is correctly logged in"
|
||||
|
||||
puts "--- End of Provisioning Profile Diagnostics ---\n"
|
||||
end
|
||||
|
||||
### Android-specific Methods ###
|
||||
|
||||
def self.android_create_keystore(keystore_path)
|
||||
if ENV["ANDROID_KEYSTORE"]
|
||||
puts "Decoding Android keystore..."
|
||||
FileUtils.mkdir_p(File.dirname(keystore_path))
|
||||
File.write(keystore_path, Base64.decode64(ENV["ANDROID_KEYSTORE"]))
|
||||
end
|
||||
|
||||
File.realpath(keystore_path)
|
||||
end
|
||||
|
||||
def self.android_create_play_store_key(key_path)
|
||||
if ENV["ANDROID_PLAY_STORE_JSON_KEY_BASE64"]
|
||||
puts "Decoding Android Play Store JSON key..."
|
||||
FileUtils.mkdir_p(File.dirname(key_path))
|
||||
File.write(key_path, Base64.decode64(ENV["ANDROID_PLAY_STORE_JSON_KEY_BASE64"]))
|
||||
end
|
||||
|
||||
File.realpath(key_path)
|
||||
end
|
||||
|
||||
# unused to do api key permissions
|
||||
def self.android_verify_version_code(gradle_file_path)
|
||||
latest_version = Fastlane::Actions::GooglePlayTrackVersionCodesAction.run(
|
||||
track: "internal",
|
||||
json_key: ENV["ANDROID_PLAY_STORE_JSON_KEY_PATH"],
|
||||
package_name: ENV["ANDROID_PACKAGE_NAME"],
|
||||
).first
|
||||
|
||||
version_code_line = File.readlines(gradle_file_path).find { |line| line.include?("versionCode") }
|
||||
current_version = version_code_line.match(/versionCode\s+(\d+)/)[1].to_i
|
||||
|
||||
if current_version <= latest_version
|
||||
report_error(
|
||||
"Version code must be greater than latest Play Store version!",
|
||||
"Latest Play Store version: #{latest_version}\nCurrent version: #{current_version}\nPlease increment the version code in android/app/build.gradle",
|
||||
"Version code verification failed"
|
||||
)
|
||||
else
|
||||
report_success("Version code verified (Current: #{current_version}, Latest Play Store: #{latest_version})")
|
||||
end
|
||||
end
|
||||
|
||||
def self.android_increment_version_code(gradle_file_path)
|
||||
gradle_file_full_path = File.expand_path(gradle_file_path, File.dirname(__FILE__))
|
||||
|
||||
unless File.exist?(gradle_file_full_path)
|
||||
UI.error("Could not find build.gradle at: #{gradle_file_full_path}")
|
||||
UI.user_error!("Please ensure the Android project is properly set up")
|
||||
end
|
||||
|
||||
# Read current version code
|
||||
gradle_content = File.read(gradle_file_full_path)
|
||||
version_code_match = gradle_content.match(/versionCode\s+(\d+)/)
|
||||
current_version_code = version_code_match ? version_code_match[1].to_i : 0
|
||||
|
||||
# TODO: fetch version code from play store when we have permissions
|
||||
new_version = current_version_code + 1
|
||||
|
||||
# Update version code in file
|
||||
if @@android_has_permissions
|
||||
updated_content = gradle_content.gsub(/versionCode\s+\d+/, "versionCode #{new_version}")
|
||||
File.write(gradle_file_full_path, updated_content)
|
||||
end
|
||||
|
||||
report_success("Version code incremented from #{current_version_code} to #{new_version}")
|
||||
|
||||
@@android_has_permissions ? new_version : current_version_code
|
||||
end
|
||||
|
||||
# Helper to log keychain diagnostics
|
||||
def self.log_keychain_diagnostics(certificate_name)
|
||||
puts "--- Fastlane Pre-Build Diagnostics ---"
|
||||
begin
|
||||
system("echo 'Running as user: $(whoami)'")
|
||||
system("echo 'Default keychain:'")
|
||||
system("security list-keychains -d user")
|
||||
system("echo 'Identities in build.keychain:'")
|
||||
# Use the absolute path expected in the GH runner environment
|
||||
keychain_path = "/Users/runner/Library/Keychains/build.keychain-db"
|
||||
system("security find-identity -v -p codesigning #{keychain_path} || echo 'No identities found or build.keychain doesn\'t exist at #{keychain_path}'")
|
||||
rescue => e
|
||||
puts "Error running security command: #{e.message}"
|
||||
end
|
||||
puts "Certificate name constructed by Fastlane: #{certificate_name}"
|
||||
puts "--- End Fastlane Diagnostics ---"
|
||||
end
|
||||
|
||||
### Slack Methods ###
|
||||
# Uploads a file to Slack using the files.upload API endpoint.
|
||||
# Handles multipart/form-data request construction.
|
||||
#
|
||||
# Args:
|
||||
# file_path (String): Path to the file to upload.
|
||||
# channel_id (String): ID of the channel to upload the file to.
|
||||
# initial_comment (String, optional): Message to post alongside the file.
|
||||
# thread_ts (String, optional): Timestamp of a message to reply to (creates a thread).
|
||||
# title (String, optional): Title for the uploaded file (defaults to filename).
|
||||
def self.upload_file_to_slack(file_path:, channel_id:, initial_comment: nil, thread_ts: nil, title: nil)
|
||||
unless SLACK_TOKEN && !SLACK_TOKEN.strip.empty?
|
||||
report_error("Missing SLACK_API_TOKEN environment variable.", "Cannot upload file to Slack without API token.", "Slack Upload Failed")
|
||||
return false
|
||||
end
|
||||
|
||||
unless File.exist?(file_path)
|
||||
report_error("File not found at path: #{file_path}", "Please ensure the file exists before uploading.", "Slack Upload Failed")
|
||||
return false
|
||||
end
|
||||
|
||||
file_name = File.basename(file_path)
|
||||
file_size = File.size(file_path)
|
||||
file_title = title || file_name
|
||||
|
||||
begin
|
||||
upload_url = nil
|
||||
file_id = nil
|
||||
|
||||
# Step 1: Get Upload URL
|
||||
with_retry(max_retries: 3, delay: 5) do
|
||||
UI.message("Step 1: Getting Slack upload URL for #{file_name}...")
|
||||
uri = URI.parse("https://slack.com/api/files.getUploadURLExternal")
|
||||
request = Net::HTTP::Post.new(uri)
|
||||
request["Authorization"] = "Bearer #{SLACK_TOKEN}"
|
||||
request.set_form_data(filename: file_name, length: file_size)
|
||||
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.use_ssl = true
|
||||
response = http.request(request)
|
||||
|
||||
unless response.is_a?(Net::HTTPSuccess)
|
||||
raise "Slack API (files.getUploadURLExternal) failed: #{response.code} #{response.body}"
|
||||
end
|
||||
|
||||
response_json = JSON.parse(response.body)
|
||||
unless response_json["ok"]
|
||||
raise "Slack API Error (files.getUploadURLExternal): #{response_json["error"]}"
|
||||
end
|
||||
|
||||
upload_url = response_json["upload_url"]
|
||||
file_id = response_json["file_id"]
|
||||
UI.message("Got upload URL and file ID: #{file_id}")
|
||||
end
|
||||
|
||||
# Step 2: Upload file content to the obtained URL
|
||||
with_retry(max_retries: 3, delay: 5) do
|
||||
UI.message("Step 2: Uploading file content to Slack...")
|
||||
upload_uri = URI.parse(upload_url)
|
||||
# Net::HTTP::Post requires the request body to be an IO object or string
|
||||
# Reading the file content here for the request body
|
||||
file_content = File.binread(file_path)
|
||||
|
||||
upload_request = Net::HTTP::Post.new(upload_uri)
|
||||
upload_request.body = file_content
|
||||
# Slack's upload URL expects the raw file bytes in the body
|
||||
# Content-Type is often application/octet-stream, but Slack might infer
|
||||
upload_request["Content-Type"] = "application/octet-stream"
|
||||
upload_request["Content-Length"] = file_size.to_s
|
||||
|
||||
upload_http = Net::HTTP.new(upload_uri.host, upload_uri.port)
|
||||
upload_http.use_ssl = true
|
||||
upload_response = upload_http.request(upload_request)
|
||||
|
||||
# Check for a 200 OK response for the file upload itself
|
||||
unless upload_response.is_a?(Net::HTTPOK)
|
||||
raise "File content upload failed: #{upload_response.code} #{upload_response.message} Body: #{upload_response.body}"
|
||||
end
|
||||
UI.message("File content uploaded successfully.")
|
||||
end
|
||||
|
||||
# Step 3: Complete the upload
|
||||
final_file_info = nil
|
||||
with_retry(max_retries: 3, delay: 5) do
|
||||
UI.message("Step 3: Completing Slack upload for file ID #{file_id}...")
|
||||
complete_uri = URI.parse("https://slack.com/api/files.completeUploadExternal")
|
||||
complete_request = Net::HTTP::Post.new(complete_uri)
|
||||
complete_request["Authorization"] = "Bearer #{SLACK_TOKEN}"
|
||||
complete_request["Content-Type"] = "application/json; charset=utf-8"
|
||||
|
||||
payload = {
|
||||
files: [{ id: file_id, title: file_title }],
|
||||
channel_id: channel_id,
|
||||
}
|
||||
payload[:initial_comment] = initial_comment if initial_comment
|
||||
payload[:thread_ts] = thread_ts if thread_ts
|
||||
|
||||
complete_request.body = payload.to_json
|
||||
|
||||
complete_http = Net::HTTP.new(complete_uri.host, complete_uri.port)
|
||||
complete_http.use_ssl = true
|
||||
complete_response = complete_http.request(complete_request)
|
||||
|
||||
unless complete_response.is_a?(Net::HTTPSuccess)
|
||||
raise "Slack API (files.completeUploadExternal) failed: #{complete_response.code} #{complete_response.body}"
|
||||
end
|
||||
|
||||
complete_response_json = JSON.parse(complete_response.body)
|
||||
unless complete_response_json["ok"]
|
||||
# Specific error handling for common issues
|
||||
if complete_response_json["error"] == "invalid_channel"
|
||||
UI.error("Error: Invalid SLACK_CHANNEL_ID: '#{channel_id}'. Please verify the channel ID.")
|
||||
elsif complete_response_json["error"] == "channel_not_found"
|
||||
UI.error("Error: Channel '#{channel_id}' not found. Ensure the bot is invited or the ID is correct.")
|
||||
end
|
||||
raise "Slack API Error (files.completeUploadExternal): #{complete_response_json["error"]} - #{complete_response_json["response_metadata"]&.[]("messages")&.join(", ")}"
|
||||
end
|
||||
|
||||
# Expecting an array of file objects
|
||||
final_file_info = complete_response_json["files"]&.first
|
||||
unless final_file_info
|
||||
raise "Upload completed but no file information returned in response: #{complete_response.body}"
|
||||
end
|
||||
report_success("Successfully uploaded and shared #{file_name} (ID: #{final_file_info["id"]}) to Slack channel #{channel_id}")
|
||||
end
|
||||
|
||||
return final_file_info # Return the first file object on success
|
||||
rescue JSON::ParserError => e
|
||||
report_error("Failed to parse Slack API response.", "Error: #{e.message}", "Slack Upload Failed")
|
||||
return false
|
||||
rescue => e
|
||||
# Include backtrace for better debugging
|
||||
report_error("Error during Slack upload process: #{e.message}", e.backtrace.join("\n"), "Slack Upload Failed")
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -25,7 +25,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>111</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string />
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use_frameworks!
|
||||
require 'tmpdir'
|
||||
|
||||
# Resolve react_native_pods.rb with node to allow for hoisting
|
||||
require Pod::Executable.execute_command('node', ['-p',
|
||||
@@ -9,7 +10,7 @@ require Pod::Executable.execute_command('node', ['-p',
|
||||
|
||||
project 'Self.xcodeproj'
|
||||
|
||||
platform :ios, '15.0'
|
||||
platform :ios, '15.0' if !ENV['ACT']
|
||||
prepare_react_native_project!
|
||||
|
||||
|
||||
@@ -98,12 +99,40 @@ target 'Self' do
|
||||
# update QKCutoutView.swift to hide OCR border
|
||||
qkCutoutView = 'Pods/QKMRZScanner/QKMRZScanner/QKCutoutView.swift'
|
||||
if File.exist?(qkCutoutView)
|
||||
text = File.read(qkCutoutView)
|
||||
# Comment out the line containing "addBorderAroundCutout()"
|
||||
new_text = text.gsub(/^(\s*addBorderAroundCutout\s*\(\s*\))/, '// \1')
|
||||
File.open(qkCutoutView, "w") { |file| file.puts new_text }
|
||||
puts "Adding build phase script to patch QKCutoutView.swift"
|
||||
phase_name = "Patch QKCutoutView to hide border"
|
||||
|
||||
# Find the QKMRZScanner target
|
||||
qkmrz_target = installer.pods_project.targets.find { |t| t.name == 'QKMRZScanner' }
|
||||
|
||||
if qkmrz_target
|
||||
# Check if the phase already exists to avoid duplicates
|
||||
unless qkmrz_target.shell_script_build_phases.any? { |bp| bp.name == phase_name }
|
||||
# Add a build phase that will patch the file during build time
|
||||
phase = qkmrz_target.new_shell_script_build_phase(phase_name)
|
||||
phase.shell_script = <<~SCRIPT
|
||||
QKCUTOUT_PATH="${PODS_TARGET_SRCROOT}/QKMRZScanner/QKCutoutView.swift"
|
||||
if [ -f "$QKCUTOUT_PATH" ]; then
|
||||
# Use sed to comment out the line with addBorderAroundCutout
|
||||
sed -i '' 's/^\\(\\s*addBorderAroundCutout\\s*(.*)\\)/\\/\\/\\1/' "$QKCUTOUT_PATH"
|
||||
echo "Successfully patched QKCutoutView.swift to hide border"
|
||||
else
|
||||
echo "Warning: Could not find QKCutoutView.swift at $QKCUTOUT_PATH"
|
||||
fi
|
||||
SCRIPT
|
||||
end
|
||||
else
|
||||
puts "Warning: Could not find QKMRZScanner target to add build phase"
|
||||
end
|
||||
else
|
||||
puts "Warning: Could not find QKCutoutView.swift at #{qkCutoutView}"
|
||||
end
|
||||
|
||||
# Disable code signing for Pod targets to avoid conflicts with main app signing
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1627,7 +1627,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNSentry (6.8.0):
|
||||
- RNSentry (6.10.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- RCT-Folly (= 2024.01.01.00)
|
||||
@@ -1647,17 +1647,17 @@ PODS:
|
||||
- ReactCodegen
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Sentry/HybridSDK (= 8.45.0)
|
||||
- Sentry/HybridSDK (= 8.48.0)
|
||||
- Yoga
|
||||
- RNSVG (15.11.1):
|
||||
- React-Core
|
||||
- segment-analytics-react-native (2.20.3):
|
||||
- React-Core
|
||||
- sovran-react-native
|
||||
- Sentry (8.45.0):
|
||||
- Sentry/Core (= 8.45.0)
|
||||
- Sentry/Core (8.45.0)
|
||||
- Sentry/HybridSDK (8.45.0)
|
||||
- Sentry (8.48.0):
|
||||
- Sentry/Core (= 8.48.0)
|
||||
- Sentry/Core (8.48.0)
|
||||
- Sentry/HybridSDK (8.48.0)
|
||||
- SentryPrivate (8.21.0)
|
||||
- SocketRocket (0.7.0)
|
||||
- sovran-react-native (1.1.3):
|
||||
@@ -1960,93 +1960,92 @@ SPEC CHECKSUMS:
|
||||
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
lottie-ios: a881093fab623c467d3bce374367755c272bdd59
|
||||
lottie-react-native: db03203d873afcbb6e0cea6a262a88d95e64a98f
|
||||
lottie-react-native: 3ffec00c889acded6057766c99adf8eaced7790c
|
||||
NFCPassportReader: e931c61c189e08a4b4afa0ed4014af19eab2f129
|
||||
OpenSSL-Universal: 84efb8a29841f2764ac5403e0c4119a28b713346
|
||||
QKMRZParser: 6b419b6f07d6bff6b50429b97de10846dc902c29
|
||||
QKMRZScanner: cf2348fd6ce441e758328da4adf231ef2b51d769
|
||||
RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740
|
||||
RCT-Folly: 34124ae2e667a0e5f0ea378db071d27548124321
|
||||
RCTDeprecation: 726d24248aeab6d7180dac71a936bbca6a994ed1
|
||||
RCTRequired: a94e7febda6db0345d207e854323c37e3a31d93b
|
||||
RCTTypeSafety: 28e24a6e44f5cbf912c66dde6ab7e07d1059a205
|
||||
React: c2830fa483b0334bda284e46a8579ebbe0c5447e
|
||||
React-callinvoker: 4aecde929540c26b841a4493f70ebf6016691eb8
|
||||
React-Core: 1e3c04337857fa7fb7559f73f6f29a2a83a84b9c
|
||||
React-CoreModules: 9fac2d31803c0ed03e4ddaa17f1481714f8633a5
|
||||
React-cxxreact: c72a7a8066fc4323ea85a3137de50c8a10a69794
|
||||
React-Core: 65374ea054f3f00eaa3c8bb5e989cb1ba8128844
|
||||
React-CoreModules: f53e0674e1747fa41c83bc970e82add97b14ad87
|
||||
React-cxxreact: bb77e88b645c5378ecd0c30c94f965a8294001d8
|
||||
React-debug: 7e346b6eeacd2ee1118a0ee7d39f613b428b4be8
|
||||
React-defaultsnativemodule: e40e760aa97a7183d5f5a8174e44026673c4b995
|
||||
React-domnativemodule: 9fef73afd600e7c7d7f540d82532a113830bbdda
|
||||
React-Fabric: dcd7ec3ea4da022b6c3f025e2567c9860ff1f760
|
||||
React-FabricComponents: 7e67af984cab1d6d1c02aae4a62933abc1baa5d3
|
||||
React-FabricImage: 77ca01a0a2bca3e1d39967220d7af7e3de033c9f
|
||||
React-defaultsnativemodule: 4f1e9236c048fce31ebaf2c9c59ad7e76fb971a1
|
||||
React-domnativemodule: 0d0e04cd8a68f3984b7b15aada7ff531dfc3c3bd
|
||||
React-Fabric: fa636eabfe3c8a3af3a9bface586956e90619ebf
|
||||
React-FabricComponents: 52382f668a934df9cef21a7893beffbe0e2b2f5e
|
||||
React-FabricImage: 69b745c0231d9360180f5e411370c6fb0c3cb546
|
||||
React-featureflags: 4c45b3c06f9a124d2598aff495bfc59470f40597
|
||||
React-featureflagsnativemodule: d37e4fe27bd4f541d6d46f05e899345018067314
|
||||
React-graphics: a2e6209991a191c94405a234460e05291fa986b9
|
||||
React-idlecallbacksnativemodule: fa07e0af59ec6c950b2156b14c73c7fce4d0a663
|
||||
React-ImageManager: 17772f78d93539a1a10901b5f537031772fa930c
|
||||
React-featureflagsnativemodule: 110c225191b3bca92694d36303385c2c299c12e5
|
||||
React-graphics: eb61d404819486a2d9335c043a967a0c4b8ca743
|
||||
React-idlecallbacksnativemodule: ca6930a17eaae01591610c87b19dbd90515f54a1
|
||||
React-ImageManager: 6652c4cc3de260b5269d58277de383cacd53a234
|
||||
React-jsc: 4d3352be620f3fe2272238298aaccc9323b01824
|
||||
React-jserrorhandler: 62af5111f6444688182a5850d4b584cbc0c5d6a8
|
||||
React-jsi: 490deef195fd3f01d57dc89dda8233a84bd54b83
|
||||
React-jsiexecutor: 13bcb5e11822b2a6b69dbb175a24a39e24a02312
|
||||
React-jsinspector: 6961a23d4c11b72f3cbeb5083b0b18cc22bc48a1
|
||||
React-jsitracing: dab78a74a581f63320604c9de4ab9039209e0501
|
||||
React-logger: d79b704bf215af194f5213a6b7deec50ba8e6a9b
|
||||
React-Mapbuffer: 42c779748af341935a63ad8831723b8cb1e97830
|
||||
React-microtasksnativemodule: 744f7e26200ea3976fef8453101cefcc08756008
|
||||
react-native-biometrics: 352e5a794bfffc46a0c86725ea7dc62deb085bdc
|
||||
react-native-cloud-storage: 4c68bc6025c3624164461e15231efb28576f78a8
|
||||
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
|
||||
react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
|
||||
react-native-nfc-manager: 5213321cf6c18d879c8092c0bf56806b771ec5ac
|
||||
react-native-quick-crypto: 07fd0ba954a08d8c6b4cd1ff8b1fd91df2b650d6
|
||||
react-native-safe-area-context: 849d7df29ecb2a7155c769c0b76849ba952c2aa3
|
||||
React-jserrorhandler: 552c5fcd2ee64307c568734b965ea082e1be25cf
|
||||
React-jsi: b187c826e5bda25afb36ede4c54c146cd50c9d6c
|
||||
React-jsiexecutor: ac8478b6c5f53bcf411a66bf4461e923dafeb0bd
|
||||
React-jsinspector: a82cfe0794b831d6e36cf0c8c07da56a0aaa1282
|
||||
React-jsitracing: e512a1023a25de831b51be1c773caa6036125a44
|
||||
React-logger: 80d87daf2f98bf95ab668b79062c1e0c3f0c2f8a
|
||||
React-Mapbuffer: b2642edd9be75d51ead8cda109c986665eae09cf
|
||||
React-microtasksnativemodule: 7ebf131e1792a668004d2719a36da0ff8d19c43c
|
||||
react-native-biometrics: 43ed5b828646a7862dbc7945556446be00798e7d
|
||||
react-native-cloud-storage: 74d1f1456d714e0fca6d10c7ab6fe9a52ba203b6
|
||||
react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-nfc-manager: a280ef94cd4871a471b052f0dc70381cf1223049
|
||||
react-native-quick-crypto: a7cc5f870f270488fee9047b56e481de85c32e33
|
||||
react-native-safe-area-context: 3e33e7c43c8b74dba436a5a32651cb8d7064c740
|
||||
React-nativeconfig: 31072ab0146e643594f6959c7f970a04b6c9ddd0
|
||||
React-NativeModulesApple: 5df767d9a2197ac25f4d8dd2d4ae1af3624022e2
|
||||
React-NativeModulesApple: 4ffcab4cdf34002540799bffbedd6466e8023c3a
|
||||
React-perflogger: 59e1a3182dca2cee7b9f1f7aab204018d46d1914
|
||||
React-performancetimeline: 3d70a278cc3344def506e97aff3640e658656110
|
||||
React-performancetimeline: 2bf8625ff44f482cba84e48e4ab21dee405d68cd
|
||||
React-RCTActionSheet: d80e68d3baa163e4012a47c1f42ddd8bcd9672cc
|
||||
React-RCTAnimation: bde981f6bd7f8493696564da9b3bd05721d3b3cc
|
||||
React-RCTAppDelegate: bc9c02d6dd4d162e3e1850283aba81bd246fc688
|
||||
React-RCTBlob: e492d54533e61a81f2601494a6f393b3e15e33b9
|
||||
React-RCTFabric: 4556aa70bd55b48d793cfb87e80d687c164298e2
|
||||
React-RCTImage: 90448d2882464af6015ed57c98f463f8748be465
|
||||
React-RCTLinking: 1bd95d0a704c271d21d758e0f0388cced768d77d
|
||||
React-RCTNetwork: 218af6e63eb9b47935cc5a775b7a1396cf10ff91
|
||||
React-RCTSettings: e10b8e42b0fce8a70fbf169de32a2ae03243ef6b
|
||||
React-RCTText: e7bf9f4997a1a0b45c052d4ad9a0fe653061cf29
|
||||
React-RCTVibration: 5b70b7f11e48d1c57e0d4832c2097478adbabe93
|
||||
React-RCTAnimation: 051f0781709c5ed80ba8aa2b421dfb1d72a03162
|
||||
React-RCTAppDelegate: 99345256dcceddcacab539ff8f56635de6a2f551
|
||||
React-RCTBlob: e949797c162421e363f93bfd8b546b7e632ba847
|
||||
React-RCTFabric: 396093d9aeee4bd3a6021ec6df8ed012f78763ef
|
||||
React-RCTImage: b73149c0cd54b641dba2d6250aaf168fee784d9f
|
||||
React-RCTLinking: 23e519712285427e50372fbc6e0265d422abf462
|
||||
React-RCTNetwork: a5d06d122588031989115f293654b13353753630
|
||||
React-RCTSettings: 87d03b5d94e6eadd1e8c1d16a62f790751aafb55
|
||||
React-RCTText: 75e9dd39684f4bcd1836134ac2348efaca7437b3
|
||||
React-RCTVibration: 033c161fe875e6fa096d0d9733c2e2501682e3d4
|
||||
React-rendererconsistency: 35cef4bc4724194c544b6e5e2bd9b3f7aff52082
|
||||
React-rendererdebug: 9b1a6a2d4f8086a438f75f28350ccba16b7b706a
|
||||
React-rendererdebug: 4e801e9f8d16d21877565dca2845a2e56202b8c6
|
||||
React-rncore: 2c7c94d6e92db0850549223eb2fa8272e0942ac2
|
||||
React-RuntimeApple: 22397aca29a0c9be681db02c68416e508a381ef1
|
||||
React-RuntimeCore: a6d413611876d8180a5943b80cba3cefdf95ad5f
|
||||
React-RuntimeApple: 0f661760cfcfa5d9464f7e05506874643e88fc2d
|
||||
React-RuntimeCore: 1d0fcc0eb13807818e79ccaf48915596f0f5f0e6
|
||||
React-runtimeexecutor: ea90d8e3a9e0f4326939858dafc6ab17c031a5d3
|
||||
React-runtimescheduler: e041df0539ad8a8a370e3507c39a9ab0571bb848
|
||||
React-utils: 768a7eb396b7df37aa19389201652eac613490cd
|
||||
ReactCodegen: c53f8a0fa088739ee9929820feec1508043c7e6c
|
||||
ReactCommon: 03d2d48fcd1329fe3bc4e428a78a0181b68068c2
|
||||
RNCAsyncStorage: 03861ec2e1e46b20e51963c62c51dc288beb7c43
|
||||
RNCClipboard: 60fed4b71560d7bfe40e9d35dea9762b024da86d
|
||||
RNDeviceInfo: feea80a690d2bde1fe51461cf548039258bd03f2
|
||||
RNGestureHandler: 5639cd6112a3aa3bebc3871e3bf4e83940e20f6f
|
||||
RNGoogleSignin: ee2938633d996756819e3212c8e0f7f696b380d0
|
||||
RNKeychain: bfe3d12bf4620fe488771c414530bf16e88f3678
|
||||
RNLocalize: 06991b9c31e7a898a9fa6ddb204ce0f53a967248
|
||||
RNReactNativeHapticFeedback: cba92e59f56506f6058d261dc85986012b2c5032
|
||||
RNScreens: d2a8ff4833a42f4eeadaea244f0bd793301b8810
|
||||
RNSentry: 7f68d46fd7f2315484bc1bdf54184d5fab48db5d
|
||||
RNSVG: 669ed128ab9005090c612a0d627dbecb6ab5c76f
|
||||
segment-analytics-react-native: d57ed4971cbb995706babf29215ebdbf242ecdab
|
||||
Sentry: f7c0c4b82e2f7e5909a660544dfaff84e35d5e03
|
||||
React-runtimescheduler: 6b33edee8c830c7926670df4232d51f4f6a82795
|
||||
React-utils: 7198bd077f07ce8f9263c05bf610da6e251058ad
|
||||
ReactCodegen: a2d336e0bec3d2f45475df55e7a02cc4e4c19623
|
||||
ReactCommon: b02a50498cb1071cd793044ddbd5d2b5f4db0a34
|
||||
RNCAsyncStorage: af7b591318005069c3795076addc83a4dd5c0a2e
|
||||
RNCClipboard: 4abb037e8fe3b98a952564c9e0474f91c492df6d
|
||||
RNDeviceInfo: d863506092aef7e7af3a1c350c913d867d795047
|
||||
RNGestureHandler: 9c3877d98d4584891b69d16ebca855ac46507f4d
|
||||
RNGoogleSignin: b8760528f2a7cbe157ecfdcc13bdb7d2745c9389
|
||||
RNKeychain: bbe2f6d5cc008920324acb49ef86ccc03d3b38e4
|
||||
RNLocalize: 15463c4d79c7da45230064b4adcf5e9bb984667e
|
||||
RNReactNativeHapticFeedback: e19b9b2e2ecf5593de8c4ef1496e1e31ae227514
|
||||
RNScreens: b7e8d29c6be98f478bc3fb4a97cc770aa9ba7509
|
||||
RNSentry: c462461c0a5aaba206265f1f3db01b237cd33239
|
||||
RNSVG: 46769c92d1609e617dbf9326ad8a0cff912d0982
|
||||
segment-analytics-react-native: 6f98edf18246782ee7428c5380c6519a3d2acf5e
|
||||
Sentry: 1ca8405451040482877dcd344dfa3ef80b646631
|
||||
SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe
|
||||
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
|
||||
sovran-react-native: eec37f82e4429f0e3661f46aaf4fcd85d1b54f60
|
||||
sovran-react-native: a3ad3f8ff90c2002b2aa9790001a78b0b0a38594
|
||||
SwiftQRScanner: e85a25f9b843e9231dab89a96e441472fe54a724
|
||||
SwiftyTesseract: 1f3d96668ae92dc2208d9842c8a59bea9fad2cbb
|
||||
Yoga: b05994d1933f507b0a28ceaa4fdb968dc18da178
|
||||
|
||||
PODFILE CHECKSUM: 4083907fcd59614c8d3d8cef569079861356d36f
|
||||
PODFILE CHECKSUM: 9eaf085590e4280f6aedd49c2efe960fbf2b4079
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
|
||||
@@ -481,7 +481,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassportDebug.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 110;
|
||||
CURRENT_PROJECT_VERSION = 111;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 5B29R5LYHQ;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -619,7 +619,7 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = OpenPassport/OpenPassport.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 110;
|
||||
CURRENT_PROJECT_VERSION = 111;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = 5B29R5LYHQ;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
|
||||
@@ -6,22 +6,40 @@
|
||||
"analyze:android": "yarn reinstall && react-native-bundle-visualizer --platform android --dev",
|
||||
"analyze:ios": "yarn reinstall && react-native-bundle-visualizer --platform ios --dev",
|
||||
"android": "react-native run-android",
|
||||
"android:build-apk": "yarn reinstall && cd ./android && ./gradlew clean assembleRelease && cd ..",
|
||||
"android:build-debug": "yarn reinstall && cd ./android && yarn android:build-debug-bundle && ./gradlew clean assembleDebug && cd ..",
|
||||
"android:build-debug-bundle": "yarn react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/",
|
||||
"android:build-release": "yarn reinstall && cd ./android && ./gradlew clean bundleRelease && cd ..",
|
||||
"android:build-apk": "yarn reinstall && cd ./android && ./gradlew clean assembleRelease && cd ..",
|
||||
"android:fastlane-debug": "yarn reinstall && bundle exec fastlane --verbose android internal_test",
|
||||
"bump-version:major": "npm version major && yarn sync-versions",
|
||||
"bump-version:minor": "npm version minor && yarn sync-versions",
|
||||
"bump-version:patch": "npm version patch && yarn sync-versions",
|
||||
"clean": "watchman watch-del-all && rm -rf node_modules ios/Pods ios/build android/app/build android/build .yarn/cache ios/.xcode.env.local",
|
||||
"clean:android": "rm -rf android/app/build android/build",
|
||||
"clean:ios": "rm -rf ios/Pods ios/build",
|
||||
"clean:xcode-env-local": "rm -f ios/.xcode.env.local",
|
||||
"fmt": "prettier --check .",
|
||||
"fmt:fix": "prettier --write .",
|
||||
"force-local-upload-deploy": "yarn force-local-upload-deploy:android && yarn force-local-upload-deploy:ios",
|
||||
"force-local-upload-deploy:android": "yarn clean:android && FORCE_UPLOAD_LOCAL_DEV=true bundle exec fastlane android deploy --verbose",
|
||||
"force-local-upload-deploy:ios": "yarn clean:ios && FORCE_UPLOAD_LOCAL_DEV=true bundle exec fastlane ios deploy --verbose",
|
||||
"force-local-upload-test": "yarn force-local-upload-test:android && yarn force-local-upload-test:ios",
|
||||
"force-local-upload-test:android": "yarn clean:android && FORCE_UPLOAD_LOCAL_DEV=true bundle exec fastlane android internal_test --verbose",
|
||||
"force-local-upload-test:ios": "yarn clean:ios && FORCE_UPLOAD_LOCAL_DEV=true bundle exec fastlane ios internal_test --verbose",
|
||||
"ia": "yarn install-app",
|
||||
"install-app": "cd ../common && yarn && cd ../app && yarn && cd ios && pod install && cd .. && yarn clean:xcode-env-local",
|
||||
"install-app": "yarn install-app:setup && cd ios && bundle exec pod install && cd .. && yarn clean:xcode-env-local",
|
||||
"install-app:deploy": "yarn install-app:setup && yarn clean:xcode-env-local",
|
||||
"install-app:setup": "cd ../common && yarn && cd ../app && yarn && cd ios && bundle install && cd ..",
|
||||
"ios": "react-native run-ios",
|
||||
"ios:fastlane-debug": "yarn reinstall && bundle exec fastlane --verbose ios internal_test",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"nice": "yarn lint:fix && yarn fmt:fix",
|
||||
"reinstall": "yarn clean && yarn install && yarn install-app",
|
||||
"start": "watchman watch-del-all && react-native start",
|
||||
"sync-versions": "bundle exec fastlane ios sync_version && bundle exec fastlane android sync_version",
|
||||
"tag:release": "node scripts/tag.js release",
|
||||
"tag:remove": "node scripts/tag.js remove",
|
||||
"test": "jest --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -37,7 +55,7 @@
|
||||
"@react-navigation/native-stack": "^7.2.0",
|
||||
"@segment/analytics-react-native": "^2.20.3",
|
||||
"@segment/sovran-react-native": "^1.1.3",
|
||||
"@sentry/react-native": "^6.8.0",
|
||||
"@sentry/react-native": "^6.10.0",
|
||||
"@stablelib/cbor": "^2.0.1",
|
||||
"@tamagui/config": "1.110.0",
|
||||
"@tamagui/lucide-icons": "1.110.0",
|
||||
|
||||
117
app/scripts/tag.js
Normal file
117
app/scripts/tag.js
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Get package version
|
||||
function getVersion() {
|
||||
const packageJson = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'),
|
||||
);
|
||||
return packageJson.version;
|
||||
}
|
||||
|
||||
// Check if working directory is clean
|
||||
function isWorkingDirectoryClean() {
|
||||
try {
|
||||
const status = execSync('git status --porcelain').toString();
|
||||
return status.trim() === '';
|
||||
} catch (error) {
|
||||
console.error('Error checking git status:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create an empty commit
|
||||
function createEmptyCommit(version) {
|
||||
try {
|
||||
execSync(`git commit --allow-empty -m "chore: release v${version}"`);
|
||||
console.log(`Created empty commit for v${version}`);
|
||||
} catch (error) {
|
||||
console.error('Error creating commit:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Create git tag
|
||||
function createTag(version) {
|
||||
try {
|
||||
execSync(`git tag v${version}`);
|
||||
console.log(`Created tag v${version}`);
|
||||
} catch (error) {
|
||||
console.error('Error creating tag:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Push tag to remote
|
||||
function pushTag(version) {
|
||||
try {
|
||||
execSync(`git push origin v${version}`);
|
||||
console.log(`Pushed tag v${version} to remote`);
|
||||
} catch (error) {
|
||||
console.error('Error pushing tag:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove tag locally and from remote
|
||||
function removeTag(version) {
|
||||
try {
|
||||
execSync(`git tag -d v${version}`);
|
||||
execSync(`git push origin :refs/tags/v${version}`);
|
||||
console.log(`Removed tag v${version}`);
|
||||
} catch (error) {
|
||||
console.error('Error removing tag:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Main function to handle commands
|
||||
function main() {
|
||||
const command = process.argv[2];
|
||||
const version = getVersion();
|
||||
|
||||
switch (command) {
|
||||
case 'commit':
|
||||
if (!isWorkingDirectoryClean()) {
|
||||
console.error(
|
||||
'Error: Working directory is not clean. Please commit or stash changes first.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
createEmptyCommit(version);
|
||||
break;
|
||||
|
||||
case 'create':
|
||||
createTag(version);
|
||||
break;
|
||||
|
||||
case 'push':
|
||||
pushTag(version);
|
||||
break;
|
||||
|
||||
case 'remove':
|
||||
removeTag(version);
|
||||
break;
|
||||
|
||||
case 'release':
|
||||
if (!isWorkingDirectoryClean()) {
|
||||
console.error(
|
||||
'Error: Working directory is not clean. Please commit or stash changes first.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
createEmptyCommit(version);
|
||||
createTag(version);
|
||||
pushTag(version);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Usage: node tag.js [commit|create|push|remove|release]');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -2915,10 +2915,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/babel-plugin-component-annotate@npm:3.2.0":
|
||||
version: 3.2.0
|
||||
resolution: "@sentry/babel-plugin-component-annotate@npm:3.2.0"
|
||||
checksum: 10c0/f9ca9c5f64b86e129bb9bb4101d16c6e274b915d415279b1a09d2b80513c7280f5802e17b8a991674390d68cfa48d5033055e1632779f82d92bf9466ee2428a6
|
||||
"@sentry/babel-plugin-component-annotate@npm:3.2.2":
|
||||
version: 3.2.2
|
||||
resolution: "@sentry/babel-plugin-component-annotate@npm:3.2.2"
|
||||
checksum: 10c0/96835b165b6f7904eb74d117fed137f724490bc919940c740113eb2d8836de6ccd977b7ec056a0ca20712e6cdb19c30a9f515ec0028ecac70c7880f0a6b9a58d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2935,66 +2935,66 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/cli-darwin@npm:2.42.1":
|
||||
version: 2.42.1
|
||||
resolution: "@sentry/cli-darwin@npm:2.42.1"
|
||||
"@sentry/cli-darwin@npm:2.42.4":
|
||||
version: 2.42.4
|
||||
resolution: "@sentry/cli-darwin@npm:2.42.4"
|
||||
conditions: os=darwin
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/cli-linux-arm64@npm:2.42.1":
|
||||
version: 2.42.1
|
||||
resolution: "@sentry/cli-linux-arm64@npm:2.42.1"
|
||||
"@sentry/cli-linux-arm64@npm:2.42.4":
|
||||
version: 2.42.4
|
||||
resolution: "@sentry/cli-linux-arm64@npm:2.42.4"
|
||||
conditions: (os=linux | os=freebsd) & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/cli-linux-arm@npm:2.42.1":
|
||||
version: 2.42.1
|
||||
resolution: "@sentry/cli-linux-arm@npm:2.42.1"
|
||||
"@sentry/cli-linux-arm@npm:2.42.4":
|
||||
version: 2.42.4
|
||||
resolution: "@sentry/cli-linux-arm@npm:2.42.4"
|
||||
conditions: (os=linux | os=freebsd) & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/cli-linux-i686@npm:2.42.1":
|
||||
version: 2.42.1
|
||||
resolution: "@sentry/cli-linux-i686@npm:2.42.1"
|
||||
"@sentry/cli-linux-i686@npm:2.42.4":
|
||||
version: 2.42.4
|
||||
resolution: "@sentry/cli-linux-i686@npm:2.42.4"
|
||||
conditions: (os=linux | os=freebsd) & (cpu=x86 | cpu=ia32)
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/cli-linux-x64@npm:2.42.1":
|
||||
version: 2.42.1
|
||||
resolution: "@sentry/cli-linux-x64@npm:2.42.1"
|
||||
"@sentry/cli-linux-x64@npm:2.42.4":
|
||||
version: 2.42.4
|
||||
resolution: "@sentry/cli-linux-x64@npm:2.42.4"
|
||||
conditions: (os=linux | os=freebsd) & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/cli-win32-i686@npm:2.42.1":
|
||||
version: 2.42.1
|
||||
resolution: "@sentry/cli-win32-i686@npm:2.42.1"
|
||||
"@sentry/cli-win32-i686@npm:2.42.4":
|
||||
version: 2.42.4
|
||||
resolution: "@sentry/cli-win32-i686@npm:2.42.4"
|
||||
conditions: os=win32 & (cpu=x86 | cpu=ia32)
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/cli-win32-x64@npm:2.42.1":
|
||||
version: 2.42.1
|
||||
resolution: "@sentry/cli-win32-x64@npm:2.42.1"
|
||||
"@sentry/cli-win32-x64@npm:2.42.4":
|
||||
version: 2.42.4
|
||||
resolution: "@sentry/cli-win32-x64@npm:2.42.4"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/cli@npm:2.42.1":
|
||||
version: 2.42.1
|
||||
resolution: "@sentry/cli@npm:2.42.1"
|
||||
"@sentry/cli@npm:2.42.4":
|
||||
version: 2.42.4
|
||||
resolution: "@sentry/cli@npm:2.42.4"
|
||||
dependencies:
|
||||
"@sentry/cli-darwin": "npm:2.42.1"
|
||||
"@sentry/cli-linux-arm": "npm:2.42.1"
|
||||
"@sentry/cli-linux-arm64": "npm:2.42.1"
|
||||
"@sentry/cli-linux-i686": "npm:2.42.1"
|
||||
"@sentry/cli-linux-x64": "npm:2.42.1"
|
||||
"@sentry/cli-win32-i686": "npm:2.42.1"
|
||||
"@sentry/cli-win32-x64": "npm:2.42.1"
|
||||
"@sentry/cli-darwin": "npm:2.42.4"
|
||||
"@sentry/cli-linux-arm": "npm:2.42.4"
|
||||
"@sentry/cli-linux-arm64": "npm:2.42.4"
|
||||
"@sentry/cli-linux-i686": "npm:2.42.4"
|
||||
"@sentry/cli-linux-x64": "npm:2.42.4"
|
||||
"@sentry/cli-win32-i686": "npm:2.42.4"
|
||||
"@sentry/cli-win32-x64": "npm:2.42.4"
|
||||
https-proxy-agent: "npm:^5.0.0"
|
||||
node-fetch: "npm:^2.6.7"
|
||||
progress: "npm:^2.0.3"
|
||||
@@ -3017,7 +3017,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
sentry-cli: bin/sentry-cli
|
||||
checksum: 10c0/2e1cfd587c7e6f4a5f5e056043eb3205ae6a716b9d1a1b690857fc5905df1785a903103510e95645a23274605ac560061183d294815c3ec45bdb0ca77726f101
|
||||
checksum: 10c0/e3900743803470874228a7d9b02f54e7973b01c89d433cd03d7d6fe71ae44df3f4420743fcae821613a572978ab1ff6fa397ab447a320bbe021bee333f04f397
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3028,13 +3028,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sentry/react-native@npm:^6.8.0":
|
||||
version: 6.8.0
|
||||
resolution: "@sentry/react-native@npm:6.8.0"
|
||||
"@sentry/react-native@npm:^6.10.0":
|
||||
version: 6.10.0
|
||||
resolution: "@sentry/react-native@npm:6.10.0"
|
||||
dependencies:
|
||||
"@sentry/babel-plugin-component-annotate": "npm:3.2.0"
|
||||
"@sentry/babel-plugin-component-annotate": "npm:3.2.2"
|
||||
"@sentry/browser": "npm:8.54.0"
|
||||
"@sentry/cli": "npm:2.42.1"
|
||||
"@sentry/cli": "npm:2.42.4"
|
||||
"@sentry/core": "npm:8.54.0"
|
||||
"@sentry/react": "npm:8.54.0"
|
||||
"@sentry/types": "npm:8.54.0"
|
||||
@@ -3048,7 +3048,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
sentry-expo-upload-sourcemaps: scripts/expo-upload-sourcemaps.js
|
||||
checksum: 10c0/6bcf9b9ca5cd855740c3300ee407421e055615f05e1bb3ecb5ef308afea873bb3795329610a2e8bccfa99343d1a37fa01627207064c44ca85c603f8412b01eb2
|
||||
checksum: 10c0/792cbb4437edea18f99bd0a5d1106523321556737758a88c742cdbdaf111eb344c736dfa4ef4a6d43213f56153714a2184dddde8d625c0932de36c9de5d32d29
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -11964,7 +11964,7 @@ __metadata:
|
||||
"@react-navigation/native-stack": "npm:^7.2.0"
|
||||
"@segment/analytics-react-native": "npm:^2.20.3"
|
||||
"@segment/sovran-react-native": "npm:^1.1.3"
|
||||
"@sentry/react-native": "npm:^6.8.0"
|
||||
"@sentry/react-native": "npm:^6.10.0"
|
||||
"@stablelib/cbor": "npm:^2.0.1"
|
||||
"@tamagui/config": "npm:1.110.0"
|
||||
"@tamagui/lucide-icons": "npm:1.110.0"
|
||||
|
||||
Reference in New Issue
Block a user