Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix flaky pod installs #46316

Merged
merged 20 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/scripts/verifyPodfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ if ! SPEC_DIRS=$(yq '.["EXTERNAL SOURCES"].[].":path" | select( . == "*node_modu
cleanupAndExit 1
fi

# Retrieve a list of podspec paths from react-native config
if ! read_lines_into_array PODSPEC_PATHS < <(npx react-native config | jq --raw-output '.dependencies[].platforms.ios.podspecPath | select ( . != null)'); then
error "Error: could not parse podspec paths from react-native config command"
cleanupAndExit 1
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/platformDeploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ jobs:
with:
timeout_minutes: 10
max_attempts: 5
command: cd ios && bundle exec pod install
command: scripts/pod-install.sh

- name: Decrypt AppStore profile
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AppStore.mobileprovision NewApp_AppStore.mobileprovision.gpg
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/testBuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ jobs:
with:
timeout_minutes: 10
max_attempts: 5
command: cd ios && bundle exec pod install --verbose
command: scripts/pod-install.sh

- name: Decrypt AdHoc profile
run: cd ios && gpg --quiet --batch --yes --decrypt --passphrase="$LARGE_SECRET_PASSPHRASE" --output NewApp_AdHoc.mobileprovision NewApp_AdHoc.mobileprovision.gpg
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"clean": "npx react-native clean-project-auto",
"android": "scripts/set-pusher-suffix.sh && npx react-native run-android --mode=developmentDebug --appId=com.expensify.chat.dev --active-arch-only",
"ios": "scripts/set-pusher-suffix.sh && npx react-native run-ios --list-devices --mode=\"DebugDevelopment\" --scheme=\"New Expensify Dev\"",
"pod-install": "cd ios && bundle exec pod install",
"pod-install": "scripts/pod-install.sh",
"ipad": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (12.9-inch) (6th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"",
"ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"",
"start": "npx react-native start",
Expand Down
89 changes: 89 additions & 0 deletions scripts/pod-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/bin/bash

# This script ensures pod installs respect Podfile.lock as the source of truth.
# Specifically, the podspecs for pods listed under the 'EXTERNAL SOURCES' key in the Podfile.lock are cached in the `ios/Pods/Local Podspecs` directory.
# While caching results in significantly faster installs, if a cached podspec doesn't match the version in Podfile.lock, pod install will fail.
# To prevent this, this script will find and delete any mismatched cached podspecs before running pod install

# Exit immediately if any command exits with a non-zero status
set -e

# Go to project root
START_DIR="$(pwd)"
ROOT_DIR="$(dirname "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)")"
cd "$ROOT_DIR" || exit 1

# Cleanup and exit
# param - status code
function cleanupAndExit {
cd "$START_DIR" || exit 1
exit "$1"
}

source scripts/shellUtils.sh

# Check if bundle is installed
if ! bundle --version > /dev/null 2>&1; then
error 'bundle is not installed. Please install bundle and try again'
cleanupAndExit 1
fi

# Check if jq is installed
if ! jq --version > /dev/null 2>&1; then
error 'jq is not installed. Please install jq and try again'
cleanupAndExit 1
fi

# Check if yq is installed
if ! yq --version > /dev/null 2>&1; then
error 'yq is not installed. Please install yq and try again'
cleanupAndExit 1
fi

CACHED_PODSPEC_DIR='ios/Pods/Local Podspecs'
if [ -d "$CACHED_PODSPEC_DIR" ]; then
info "Verifying pods from Podfile.lock match local podspecs..."

# Convert podfile.lock to json since yq is missing some features of jq (namely, if/else)
PODFILE_LOCK_AS_JSON="$(yq -o=json ios/Podfile.lock)"

# Retrieve a list of pods and their versions from Podfile.lock
declare PODS_FROM_LOCKFILE
if ! read_lines_into_array PODS_FROM_LOCKFILE < <(jq -r '.PODS | map (if (.|type) == "object" then keys[0] else . end) | .[]' < <(echo "$PODFILE_LOCK_AS_JSON")); then
error "Error: Could not parse pod versions from Podfile.lock"
cleanupAndExit 1
fi

for CACHED_PODSPEC_PATH in "$CACHED_PODSPEC_DIR"/*; do
if [ -f "$CACHED_PODSPEC_PATH" ]; then
# The next two lines use bash parameter expansion to get just the pod name from the path
# i.e: `ios/Pods/Local Podspecs/hermes-engine.podspec.json` to just `hermes-engine`
# It extracts the part of the string between the last `/` and the first `.`
CACHED_POD_NAME="${CACHED_PODSPEC_PATH##*/}"
CACHED_POD_NAME="${CACHED_POD_NAME%%.*}"

info "🫛 Verifying local pod $CACHED_POD_NAME"
CACHED_POD_VERSION="$(jq -r '.version' < <(cat "$CACHED_PODSPEC_PATH"))"
for POD_FROM_LOCKFILE in "${PODS_FROM_LOCKFILE[@]}"; do
# Extract the pod name and version that was parsed from the lockfile. POD_FROM_LOCKFILE looks like `PodName (version)`
IFS=' ' read -r POD_NAME_FROM_LOCKFILE POD_VERSION_FROM_LOCKFILE <<< "$POD_FROM_LOCKFILE"
if [[ "$CACHED_POD_NAME" == "$POD_NAME_FROM_LOCKFILE" ]]; then
if [[ "$POD_VERSION_FROM_LOCKFILE" != "($CACHED_POD_VERSION)" ]]; then
clear_last_line
info "⚠️ found mismatched pod: $CACHED_POD_NAME, removing local podspec $CACHED_PODSPEC_PATH"
rm "$CACHED_PODSPEC_PATH"
echo -e "\n"
fi
break
fi
done
clear_last_line
fi
done
fi

cd ios || cleanupAndExit 1
bundle exec pod install
Copy link
Contributor

@AndrewGable AndrewGable Jul 29, 2024

Choose a reason for hiding this comment

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

So after this script runs, will it execute like:

  • npm install where it does not verify any changes against the lock file? And it will install any local changes?

OR

  • npm ci where it will fail if there are any changes from what the lock file says?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I fully understand the question. npm ci will automatically treat package-lock.json as the source of truth, overwriting any cached node_modules if their version does not match.

pod install doesn't work the same way. It will throw an error if there's a cached pod with a different version from Podfile.lock. The goal of this PR is to write a script that works more like npm ci, where if there's a cached Pod that doesn't match, it's deleted and replaced by the version stored in Podfile.lock.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I did to test this was:

  1. Manually change the version in ios/Pods/Local Podspecs/hermes-engine.podspec.json to not match the hermes-engine version from Podfile.lock.
  2. Run cd ios && bundle exec pod install and verify that it fails
  3. Run rm ios/Pods/Local Podspecs/hermes-engine.podspec.json
  4. Run cd ios && bundle exec pod install and verify that it passes.


# Go back to where we started
cleanupAndExit 0
5 changes: 5 additions & 0 deletions scripts/shellUtils.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ function title {
printf "\n%s%s%s\n" "$TITLE" "$1" "$RESET"
}

# Function to clear the last printed line
clear_last_line() {
echo -ne "\033[1A\033[K"
}

function assert_equal {
if [[ "$1" != "$2" ]]; then
error "Assertion failed: $1 is not equal to $2"
Expand Down
2 changes: 1 addition & 1 deletion workflow_tests/assertions/platformDeployAssertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ function assertIOSJobExecuted(workflowResult: Step[], didExecute = true, isProdu
createStepAssertion('Install cocoapods', true, null, 'IOS', 'Installing cocoapods', [
{key: 'timeout_minutes', value: '10'},
{key: 'max_attempts', value: '5'},
{key: 'command', value: 'cd ios && bundle exec pod install'},
{key: 'command', value: 'scripts/pod-install.sh'},
]),
createStepAssertion('Decrypt AppStore profile', true, null, 'IOS', 'Decrypting profile', null, [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]),
createStepAssertion('Decrypt AppStore Notification Service profile', true, null, 'IOS', 'Decrypting profile', null, [{key: 'LARGE_SECRET_PASSPHRASE', value: '***'}]),
Expand Down
2 changes: 1 addition & 1 deletion workflow_tests/assertions/testBuildAssertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ function assertIOSJobExecuted(workflowResult: Step[], ref = '', didExecute = tru
[
{key: 'timeout_minutes', value: '10'},
{key: 'max_attempts', value: '5'},
{key: 'command', value: 'cd ios && bundle exec pod install --verbose'},
{key: 'command', value: 'scripts/pod-install.sh'},
],
[],
),
Expand Down
Loading