React Native) shell script로 배포 고도화하기

2ast·2024년 4월 11일
0

fastlane에 shell script 도입 배경

프로젝트를 관리할 때 fastlane을 도입해서 배포 과정을 자동화해놓는 편인데, 보통 staging과 production 환경을 나누고 version을 조합해서 사용했다.

fastlane staging version:patch
fastlane production version:minor
fastlane production version:2.1.1

하지만 이대로 쓰기에는 꽤나 불편했다. ios / android 각각의 플랫폼마다 독립적으로 호출해주어야 했고, 환경과 버전을 매번 명시해줘야했기 때문이다. 가령 하나의 피쳐 개발을 끝내고 patch 버전을 올려 배포하고자하면 다음 과정을 거쳐야만 했다.

cd ios
fastlane staging version:patch
fastlane production version:maintain
cd ..
cd android
fastlane staging version:patch
fastlane production version:maintain
cd ..

매번 이런 명령어를 작성하기 귀찮았기 때문에 이후에는 package.json에 미리 커맨드를 지정해놓고 사용했다.

fastlane-staging-patch-ios:...
...
fastlane-staging-minor:...
fastlane-staging-major:...
...
fastlane-patch:...

문제는 커맨드의 경우의 수가 너무 많아져서 코드가 장황해졌고, 커맨드가 뭐였는지 나조차도 혼란스러웠던 경험이 많아졌다는 점이었다. android에서만 staging으로 patch 버전을 올려서 배포하는 커맨드가 fastlane-patch-android-staging인지, fastlane-android-staging-patch 인지 헷갈렸고, 호출하려고보니 미처 정의하지 못한 케이스였던 적도 있었다. 뿐만 아니라 "x,y,z" 꼴로 버전을 명시하고 싶어도 이와같은 시스템에서는 불가능했다.
이런 이유로 shell script 파일을 만들어 대응하고자 했다. 최종 목표는 다음과 같이 호출을 간소화 하는 것이었다.

npm run bump patch
npm run bump ios 2.1.1

npm run dist android stating
npm run dist production ios
npm run dist

version bump script

fastlane lane 분리

이 작업을 위해 가장 먼저 한 일은, fastlane의 lane을 재구성하는 것이었다. 기존에는 하나의 lane에서 version bump와 배포를 함께 수행했는데, 스크립트 고도화를 위해 일단 두 작업 사이의 의존성을 제거해주었다.

 desc "Bump App Version"
   lane :bump do |options|
   ...
 end

 desc "build and distribute staging app"
   lane :staging do
   ...
 end
 
 desc "build and distribute production app"
   lane :production do
   ...
 end

script 스펙 정의

나는 아래와같이 platform과 version을 명시해줌으로써 스크립트를 호출해서 app version을 수정하고 싶었다.

npm run bump ios patch
npm run bump android 1.2.3
npm run bump minor

이 내용을 기반으로 스크립트의 help brief를 작성해줬다.

-h, --help                 show brief help

optional) select platform to distribute(android, ios, both). default is both
    android
    ios

required) specify build version or bump type
    patch                  increment patch version (1.0.0 -> 1.0.1)
    minor                  increment minor version (1.0.1 -> 1.1.0)
    major                  increment major version (1.1.0 -> 2.0.0)
    x.y.z                  specify a version number(1.0.0 -> x.y.z)

script flow 정리하기

전체적인 shell script의 플로우는 다음과 같다.
1. selected_platform과 new_version이라는 변수를 파일 상단에 선언해준다.
2. selected_platform의 초기값은 'both'로, 외부에서 값을 입력받지 않으면 기본적으로 ios와 android에서 모두 실행한다.
3. arg로 "ios"나 "android"가 들어온다면 selected_platform에 이 값을 할당한다.
4. arg로 "patch", "minor", "major", 또는 "x.y.z" 꼴로 버전 정보가 들어오면 이 값을 new_version 변수에 할당한다.
5. 만약 new_version 변수에 값이 할당되지 않았다면 스크립트를 종료한다.
6. 만약 모든 변수에 올바른 값이 할당되었다면 적절하게 fastlane bump를 실행한다.

실제 스크립트로 작성하기


# 변수 초기화
selected_platform="both"
new_version=""

# args의 갯수가 0보다 크다면 조건문을 돌아라
while test $# -gt 0; do
  # 현재 처리중인 arg를 "arg"라는 이름의 변수에 할당(가독성을 위한 작업)
  arg="$1"


  # 만약 arg가 x.y.z 꼴의 버전이라면 new_version 변수에 할당
  if [[ "$arg" =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
    new_version="$arg"
    shift #현재 바라보는 arg 제거.
    # shift된 최신 arg를 다시 할당하여 아래 case문에서 최신 arg를 바라보도록 한다.
    arg="$1"
  fi


  case "$arg" in
    -h|--help)
      echo "-h, --help                 show brief help"
      echo ""
      echo "optional) select platform to distribute(android, ios, both). default is both"
      echo "    android"
      echo "    ios"
      echo ""
      echo "required) specify build version or bump type"
      echo "    patch                  increment patch version (1.0.0 -> 1.0.1)"
      echo "    minor                  increment minor version (1.0.1 -> 1.1.0)"
      echo "    major                  increment major version (1.1.0 -> 2.0.0)"
      echo "    x.y.z                  specify a version number(1.0.0 -> x.y.z)"
      exit 0
      ;;
    android)
      selected_platform="android"
      shift
      ;;
    ios)
      selected_platform="ios"
      shift
      ;;
    patch)
      new_version="patch"
      shift
      ;;
    minor)
      new_version="minor"
      shift
      ;;
    major)
      new_version="major"
      shift
      ;;
    *)
      break
      ;;
  esac

done



# 초기화 이후 new_version 값이 할당된 적 없다면 error message와 함께 스크립트 종료
if [ "$new_version" = "" ]; then
  echo "       bump_type can be 'major', 'minor', 'patch', or a specific version number like 'x.y.z'"
  exit 1
fi

if [ "$selected_platform" = "both" -o "$selected_platform" = "ios"  ]; then
  #조건에 맞춰 미리 정의해둔 fastlane bump lane을 실행한다.
  cd ios && fastlane bump version:$new_version && cd ..
fi

if [ "$selected_platform" = "both" -o "$selected_platform" = "android" ]; then
  #조건에 맞춰 미리 정의해둔 fastlane bump lane을 실행한다.
  cd android && fastlane bump version:$new_version && cd ..
fi

package.json에 script 명시

나는 sh파일들은 ./sh/ 경로에 모아놓았다. 이를 쉽게 호출하기 위해 package.json에 정의해주었다.

scripts:{
	"bump": "bash sh/bump.sh",
}

note

  • fastlane bump에 해당하는 코드가 궁금하다면 android fastlane 배포 자동화ios fastlane 배포 자동화를 읽어보면 좋다.
  • sh에서 exit 0은 성공으로 인한 종료, exit 1은 실패로 인한 종료를 의미한다.
  • arg를 number 정규식으로 판단하기 위해 x.y.z꼴로 직접 버전을 지정하는 조건문을 case문이 아니라 상단 if문으로 분리해주었다.
  • package.json에 스크립트를 넣으면 option을 호출할 때 --를 추가로 붙어야주어야 한다.
    npm run bump -- --help

distribute script

script 스펙 정의

나는 하나의 앱을 production과 staging으로 묶어 관리하고 있었고, 이를 다시 platform에 따라 자유롭게 빌드 환경을 선택하고 싶었다. 만약 env와 platform을 생략한다면 모든 케이스에 대해서 배포를 진행되어야 한다.

npm run dist ios staging
npm run dist android prod
npm run dist production
npm run dist android
npm run dist

이 내용을 기반으로 스크립트의 help brief를 작성해줬다.

-h, --help                 show brief help

optional) select platform to distribute(android, ios, both). default is both
    android
    ios

optional) specify a environment to distribute(staging, production|prod, both). default is both
    production|prod        build and distribute with production environment
    staging                build and distribute with staging environment

script flow 정리하기

  1. selected_platform과 selected_env라는 변수를 파일 상단에 선언해준다.
  2. 두 변수 모두 초기값은 'both'로, 외부에서 값을 입력받지 않으면 기본적으로 모든 경우의 수에 대해서 실행한다.
  3. arg로 "ios"나 "android"가 들어온다면 selected_platform에 이 값을 할당한다.
  4. arg로 "staging"이나 "production", "prod"가 들어온다면 selected_env에 이 값을 할당한다.
  5. 만약 올바른 arg가 입력되지 않았으면 error message와 함께 스크립트 종료
  6. 할당된 변수를 기반으로 적절한 fastlane lane을 실행.
  7. ios 빌드인 경우 실제 찍히는 로그를 계속 지켜보다가 "Waiting for the build to show up in the build list"가 포함된 줄이 출력되는 순간 현재 실행중인 명령어를 종료(사실상 업로드 종료 후 테스트플라이트에 처리되는 과정을 기다릴 때 출력되는 문구.)
# 변수 초기화
selected_platform="both"
selected_env="both"

# arg가 1개 이상일 경우 반복문 실행
while test $# -gt 0; do
  arg="$1"
  case "$1" in
    -h|--help)
      echo "-h, --help                 show brief help"
      echo ""
      echo "optional) select platform to distribute(android, ios, both). default is both"
      echo "    android"
      echo "    ios"
      echo ""
      echo "optional) specify a environment to distribute(staging, production|prod, both). default is both"
      echo "    production|prod        build and distribute with production environment"
      echo "    staging                build and distribute with staging environment"

      exit 0
      ;;
    android)
      selected_platform="android"
      shift
      ;;
    ios)
      selected_platform="ios"
      shift
      ;;
    production|prod)
      selected_env="production"
      shift
      ;;
    staging)
      selected_env="staging"
      shift
      ;;
    *)
      echo "Error: '$arg' is not one of the predefined options."
      exit 1
      ;;
  esac
done

run_command_and_monitor_output() {
    # 외부에서 입력받은 명령어 실행하고 표준 출력을 임시 파일에 리다이렉트
    # 명령어가 실행되는 동안 아래 라인이 돌아야하므로, "&"를 붙여 백그라운드 실행
    $1 > ./output.txt &

    # 해당 명령어의 PID 저장
    PID=$!

    # 로그를 모니터링하고 특정 패턴을 찾는 함수
    monitor_output() {
        tail -n 0 -f ./output.txt | while read line; do
          # 명령어가 백그라운드에서 돌기 때문에 직접 로그를 찍어준다.
          echo "$line"
          # 로그에서 아래 문구로 시작하는 패턴 찾기
          if [[ $line == *"Waiting for the build to show up in the build list"* ]]; then
              echo "Pattern found: $line"
              # 해당 패턴을 찾으면 현재 실행중인 명령어를 종료
              kill $PID
              break
          fi
        done
    }

    # 로그를 모니터링하고 패턴을 찾는 함수 호출
    monitor_output
    rm ./output.txt  # 임시 파일 삭제
}



# 변수에 할당된 값에 따라 적절한 위치에서 적절한 lane을 실행.
if [ "$selected_platform" = "both" -o "$selected_platform" = "ios" ]; then
  cd ios
  if [ "$selected_env" = "both" -o "$selected_env" = "staging" ]; then
    run_command_and_monitor_output "fastlane staging"
  fi

  if [ "$selected_env" = "both" -o "$selected_env" = "production" ]; then
    run_command_and_monitor_output "fastlane production"
  fi
  cd ..
fi

if [ "$selected_platform" = "both" -o "$selected_platform" = "android" ]; then
  cd android
  if [ "$selected_env" = "both" -o "$selected_env" = "staging" ]; then
    fastlane staging
  fi

  if [ "$selected_env" = "both" -o "$selected_env" = "production" ]; then
    fastlane production
  fi
  cd ..
fi

package.json에 script 명시

scripts:{
	"dist": "bash sh/distribute.sh",
}

note

  • run_command_and_monitor_output 안쪽에 정의된 monitor_output은 tail을 이용해 임시파일의 line을 읽어들인다. 여기서 tail은 무한루프로 동작하므로 반드시 내부에서 break를 해주어야한다. 위 코드는 성공 케이스만을 상정하고 "Waiting for the build to show up in the build list"만을 판단하고 있지만, 실제로는 실패 케이스를 상정하고 "fastlane finished with errors" 따위의 실패 로그도 함께 체크해주어야 한다.

codepush script

위와같은 플로우로 codepush까지 스크립트로 간소화 해주었다.


selected_platform="both"
selected_env="both"
target_version=""

while test $# -gt 0; do
  arg="$1"

  case "$arg" in
    -h|--help)
      echo "-h, --help                 show brief help"
      echo ""
      echo "-t                         specify target version. range Expression is available (-t '1.2.3 - 1.3.*')"
      echo ""
      echo "optional) select platform to distribute(android, ios, both). default is both"
      echo "    android"
      echo "    ios"
      echo ""
      echo "optional) specify a environment to distribute(staging, production|prod, both). default is both"
      echo "    production|prod        build and distribute with production environment"
      echo "    staging                build and distribute with staging environment"
      exit 0
      ;;
    -t)
      shift
      target_version="$1"
      shift
      ;;
    android)
      selected_platform="android"
      shift
      ;;
    ios)
      selected_platform="ios"
      shift
      ;;
    production|prod)
      selected_env="production"
      shift
      ;;
    staging)
      selected_env="staging"
      shift
      ;;
    *)
      break
      ;;
  esac

done

if [ "$selected_platform" = "both" -o "$selected_platform" = "ios"  ]; then
  if [ "$selected_env" = "both" -o "$selected_env" = "production" ]; then
    appcenter codepush release-react -a <ownerName>/<appName> -d <deploymentName> -t $target_version
  fi
  if [ "$selected_env" = "both" -o "$selected_env" = "staging" ]; then
    appcenter codepush release-react -a <ownerName>/<appName> -d <deploymentName> -t $target_version
  fi
fi

if [ "$selected_platform" = "both" -o "$selected_platform" = "android" ]; then
  if [ "$selected_env" = "both" -o "$selected_env" = "production" ]; then
    appcenter codepush release-react -a <ownerName>/<appName> -d <deploymentName> -t $target_version
    cd android && ./gradlew clean && cd ..
  fi
  if [ "$selected_env" = "both" -o "$selected_env" = "staging" ]; then
    appcenter codepush release-react -a <ownerName>/<appName> -d <deploymentName> -t $target_version
    cd android && ./gradlew clean && cd ..
  fi
fi

package.json에 script 명시

scripts:{
	"codepush": "bash sh/codepush.sh",
}
npm run codepush android
npm run codepush staging
npm run codepush prod -- -t "3.1.*"
npm run codepush android staging -- -t "1.1.0 - 1.1.1"
npm run codepush
profile
React-Native 개발블로그

5개의 댓글

comment-user-thumbnail
2024년 4월 23일

help brief 넘기면 gpt 가 기깔나게 뽑아줄것 같네요 아이디어 감사합니다
근데 이제 쉘스크립트 유지보수 누가하냐

1개의 답글
comment-user-thumbnail
2024년 6월 21일

안녕하세요. 스타트업 사이드프로젝트로 크로스플랫폼 초기맴버분을 찾고 있는데요. 혹시 관심 있으신가요?
관심 있으시다면 아래의 링크를 통해 어떤 주제와 내용인지 이야기해볼 수 있을 것 같습니다.
https://holaworld.io/study/667240d7e3d9c40013f95ab1

1개의 답글