Deploying for iOS is typically a tedious and time-consuming task. You have to open the Xcode, change the version name and number, get an archive, press a bunch of buttons, wait for each step and finally hit upload.
Today, we'll use Python to automate this process.
Note: This tutorial is for Flutter 3+, and I'm automating it with Python 3. I also worked on a bash version, but it's incomplete. You can get the entire script and bash version from this gist
3rd Party tools
There are a lot of articles and tools like Fastlane or Codemagic Cli that can help you to automate this process.
We are not going to use them in this tutorial. We do it ourselves, which will give us more flexibility in the long run.
In some projects, devs always release from the main branch, and it's easy to have the latest version number in the pubspec.yaml
file and keep it update.
In other projects, you have to send a TestFlight version from different feature branches, and each time you want to deploy a new version, you must check AppStore Connect to see what the most recent version is.
Step 2 to 4 is about automating this problem. In these sections, we will request to get the latest available version from the Apple server and ask the developer what version they want to set.
Step 1: Configure Xcode
Before automating this process, we must make sure that we can build our app manually and project settings are correct. I will not include these steps in this tutorial because flutter documentation explained it very well.
Step 2: Get the API Key from Apple
To get the latest version from Apple's server, we can use App Store Connect API. But before we can use this API, We must first request to get access to API Key from Users and Access > Keys > App Store Connect API. This action is only available to the Account Holder role.
After requesting access, you need to generate a new API Key.
Hit the (+) button, Enter a name and choose the access. In the access field, make sure to select the Developer
role because it has the ability to upload IPA files to the Apple Store.
After generating API Key, you can see it in the Active section. Click on Download API Key
to get the .p8 file.
Step 3: Create JWT
After obtaining all required credentials, it is time to generate a JWT token for use with the Apple Store Connect API.
Note: You can find your App ID from the URL when opening the app page.
For example, In appstoreconnect.apple.com/apps/1555XXXXX, 1555XXXXX is your App ID
To begin, declare credentials in the code as follows:
ISSUER_ID = '9f4xxxx-xxxx-xxxx-xxxx-xxxxxxxxx'
KEY_ID = '2F9XXXXXXX'
PRIVATE_KEY_PATH = './private_keys/AuthKey_2F9XXXXXXX.p8'
APP_ID = '1500000000'
With this information, we can now construct JWT as follows:
EXP_IN_SEC = 60
with open(PRIVATE_KEY_PATH, 'r+b') as f:
private_key = f.read()
expiration_time = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(seconds=EXP_IN_SEC)
encoded_jwt = jwt.encode(
{
'iss': ISSUER_ID,
'exp': expiration_time,
'aud': 'appstoreconnect-v1'
},
private_key,
algorithm='ES256',
headers={
'alg': 'ES256',
'kid': KEY_ID,
'typ': 'JWT',
}
)
Note: In this section, we use a library to generate JWT, which must be installed as follows: 'pip install pyjwt'
Step 4: Get the last version
Now we can use the JWT we generated earlier to get the most recent TestFlight build.
BASE_URL = 'https://api.appstoreconnect.apple.com/v1'
headers = {'Authorization': f'Bearer {encoded_jwt}'}
r = requests.get(f'{BASE_URL}/preReleaseVersions', headers=headers, params={'limit': 1, 'filter[app]': APP_ID}).json()
build_id = r['data'][0]['id']
build_name = r['data'][0]['attributes']['version']
r = requests.get(f'{BASE_URL}/preReleaseVersions/{build_id}/builds', headers=headers).json()
build_number = r['data'][0]['attributes']['version']
print(f'App version: {build_name}')
print(f'Build number: {build_number}')
We want the developer to determine the next version after we get the last build number (like 154) and build name (like 2.3.0). Most of the time, we only need to increase the build number by one, which is why I included the if-else clause. If you leave the build number blank and press enter, it will add one to the previous build number.
new_build_name = input('Enter new app version: ')
new_build_number_str = input('Enter new build number(enter to auto increment): ')
if new_build_number_str == '':
new_build_number = int(build_number) + 1
else:
new_build_number = int(new_build_number_str)
if new_build_name == '':
exit('No new version number entered')
Step 5: Build the app
Now it's time to build the app, and we no longer require Python. We need the power Flutter cli to rescue us :)
To run the terminal commands from python, we can use subprocess.run
in python.
def run(command: str, check: bool = None, output: bool = True, cwd: str = None):
return subprocess.run(shlex.split(command), check=check, stdout=subprocess.DEVNULL if not output else None, cwd=cwd)
Before building the IPA file, we have to make sure all the packages are updated and we have everything we need to build the project.
# Update dependencies
print('Getting pub...')
run('flutter pub get', check=True, output=False)
print('Installing pod dependencies...')
run('rm ios/Podfile.lock', check=True, output=False)
run('pod install --repo-update', cwd="ios/", check=True, output=False)
After that, we use the following function to read the pubspec.yaml
file and change the version: x.x.x+yyy
with the new version we specified earlier in the input.
def change_pub_version(version: str):
with open('pubspec.yaml', 'r') as f:
data = f.read()
data = re.sub(r'^(version:\s*)(.+)', f'version: {version}', data, flags=re.MULTILINE)
f.close()
with open('pubspec.yaml', 'w') as f:
f.write(data)
f.close()
We can also remove the build folder to ensure there is nothing else in there before asking Flutter to create the IPA file for us. If you are using Flutter version 3+, You don't need to do anything; Flutter handles generating IPA for you.
# you can also specify version when building like this:
# `flutter build ipa --build-name {new_app_version} --build-number {new_build_number}`
change_pub_version(f'{new_build_name}+{new_build_number}')
run('rm -rf build/ios')
run(f'flutter build ipa --no-pub', check=True)
Note: by default, flutter auto-generates some parameters from the pubspec file when building. If you haven't changed anything in the Xcode project, it will get the version from those parameters.
If it doesn't work for you, you can follow this article or this answer to fix it.
Step 6: Upload it to AppStore
We successfully generated an IPA file in the previous step. It's now time to upload it. At this point, there are two options for manually uploading it, but what's the fun in that?
We use altool
to upload the file for us. We also consider that uploading or verifying the file may fail, and in that case, we open the folder and the Transporter application, so we can do it manually and determine what caused the error.
e = run(f'xcrun altool --upload-app --type ios -f build/ios/ipa/*.ipa --apiKey {KEY_ID} --apiIssuer {ISSUER_ID}')
if e.returncode != 0:
run('open build/ios/ipa')
run('open -n /Applications/Transporter.app')
Step 7: Add script to IDE
Now that we've finished our Python script, let's make it easier to run. You can, of course, run it from the terminal, but there are better ways.
Android studio
It is possible to add it as a run configuration and press run whenever you need it. Ensure that the Working directory
is the root path of your project.
VSCode
You can store it in the .vscode/launch.json
file as a run configuration.
{
"version": "0.2.0",
"configurations": [
{
"name": "Deploy iOS",
"type": "python",
"request": "launch",
"program": "scripts/deploy_ios.py",
"console": "integratedTerminal"
}
]
}
Step 8: Get rid of "Manage compliance"
After uploading each TestFlight version to AppStore Connect, you will see the Manage compliance
warning. Apple won't roll out the new release for TestFlight users until you answer those two questions in the box.
This can be annoying when you want an automated process from start to finish. Luckily, this answer will help you get rid of it.
Bouns: Flavors
Flavors are an excellent way to separate different environments. I recommend reading this article for implementing them in Flutter.
After adding different flavors to your project, you can create a new app in AppStore Connect for the dev version with different bundle identifier.
In this gist, I added an example for when you use different bundle identifier and you need to deploy them separately.
Top comments (2)
Keep up the good work!
gggggreat