Skip to content

Conversation

ccaruceru
Copy link
Contributor

@ccaruceru ccaruceru commented May 11, 2024

Checklist

  • I've run bundle exec rspec from the root directory to see all new and existing tests pass
  • I've followed the fastlane code style and run bundle exec rubocop -a to ensure the code style is valid
  • I see several green ci/circleci builds in the "All checks have passed" section of my PR (connect CircleCI to GitHub if not)
  • I've read the Contribution Guidelines
  • I've updated the documentation if necessary.
  • I've added or updated relevant unit tests.

Motivation and Context

Make Testflight upload work from Linux using Transporter and potentially reduce CI costs on systems where running on a macOS is expensive (like Github Actions). This requires having Transporter installed on the machine.

Description

Add support for uploading an ipa to App Store Connect via api key when running fastlane pilot from a Linux machine. In order for this to happen, two major changes are needed:

  • adapt the ShellScriptTransporterExecutor (and surrounding code) to support API keys in the upload method
  • add missing information to the generated metadata.xml file in the .itmsp folder so that Transporter's asset validation process passes. This means the -f <.itmsp> argument works without having to use -assetFile <ipa> in combination with the -assetDescription <plist> arguments (which improves users experience on linux).

In this PR:

  • read plist data w/o an intermediate file in IpaFileAnalyser (see CFPropertyList::List.initialize())
    • this was necessary to avoid conflicts while mocking Dir.mktmpdir from existing tests
  • change BuildManager, IpaUploadPackageBuilder and the XMLTemplate.xml.erb to add bundle identifier, short version and bundle version values
  • generate the private key (p8) file in a new TransporterExecutor.prepare method and call that before every execute command to create the file on disk in different places, depending on what kind of executor instance we deal with
    • might not be the best approach, I'm open for suggestions
  • use the api key args in ShellScriptTransporterExecutor.upload and make sure only one of user/pass, jwt or api key is used at any moment
  • add new TransporterExecutor.build_credential_params method to create a list of credential parameters that each subclass will implement
  • avoid prompting the user for user/pass in ItunesTransporter.initialize when api key is not nil
  • add additional test in itunes_transporter_spec.rb and adapt tests in build_manager_spec.rb to use a real ipa and expect new calls and values
  • updated docs with pilot instructions on Linux
  • add/adapt tests

Tested on Linux (Debian 12) and MacOS (Sonoma 14).

Testing Steps

FASTLANE_ITUNES_TRANSPORTER_PATH=/usr/local/itms FASTLANE_ITUNES_TRANSPORTER_USE_SHELL_SCRIPT=true [bundle exec] fastlane run pilot ipa:<path-to-my.ipa> api_key_path:<my-app-store-key.json>

ccaruceru added 2 commits May 10, 2024 21:02
- add extra values to XMLTemplate.xml.erb and the IpaUploadPackageBuilder generator to support uploads from Linux
- read straight from plist_data in IpaFileAnalyser
- adapt tests
- update readme
- make sur only 1 of auth args are used in the ShellScriptTransporterExecutor upload command
- prevent ItunesTransporter asking for user input when jwt or api key is present
- add extra test to expect the api key in the shell script command
@ccaruceru
Copy link
Contributor Author

Let me know what are your thoughts on these additional observations and if they should be addressed here, in a follow up PR, or not at all:

  • I added api key support only upload command since that's what fastalne is usually used most for. There are other commands that could be potentially adapted, like download, verify and provider_ids
  • I haven't changed PkgUploadPackageBuilder to write same additional data in metadata.xml like IpaUploadPackageBuilder does

@@ -686,7 +722,7 @@ def initialize(user = nil, password = nil, use_shell_script = false, provider_sh
use_shell_script ||= Helper.windows?
use_shell_script ||= Feature.enabled?('FASTLANE_ITUNES_TRANSPORTER_USE_SHELL_SCRIPT')

if jwt.to_s.empty?
if jwt.to_s.empty? && api_key.nil?
Copy link
Contributor Author

Choose a reason for hiding this comment

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

can anyone think of any reason to continue asking the user for input when the api key is set?

@ccaruceru ccaruceru changed the title Ability to upload ipas to Testflight from Linux [pilot] ability to upload ipas to Testflight from Linux May 11, 2024
@ccaruceru
Copy link
Contributor Author

@AliSoftware maybe? 👀

@andreasbrgmn
Copy link

Any progress on this? This would make our workflow much smoother 😄

@ccaruceru
Copy link
Contributor Author

@rogerluan maybe you can have a look? 🙏🏻

@SomeRandomiOSDev
Copy link

Is there any update on this PR? MacOS runners are currently eating up a lot of our CI budget and it would be awesome to be able to perform at least this step using a Linux machine

@brentleyjones
Copy link
Contributor

What's needed to get this PR merged?

@SomeRandomiOSDev
Copy link

What's still needed to get this PR merged through and released?

@ccaruceru
Copy link
Contributor Author

What's still needed to get this PR merged through and released?

mostly for the maintainers to come back from vacation and review the PR 😕

in the meantime, if you want to give a shot to the changes I made then create the following Gemfile:

source 'https://rubygems.org'
gem 'fastlane', git: 'https://github.com/ccaruceru/fastlane.git', branch: 'feature/update-linux-itms-transporter'

do a bundle install, then run fastlane as described in the "Testing Steps" above.

@SomeRandomiOSDev
Copy link

@AliSoftware @joshdholtz @lacostej could we get some eyes on this PR? It seems like a number of people are waiting on this functionality in our use of fastlane

@lucasfernog-crabnebula
Copy link

ohhh I would love to use this too! I'll test it today

@lucasfernog-crabnebula
Copy link

it works!!!

@SomeRandomiOSDev
Copy link

@AliSoftware @joshdholtz @lacostej bumping this again 🙏🏼

@bcg00ding
Copy link

I'm going to bump this review up as well as this would really help us out if this work. I'm going to guess that this also then works for deliver as well if it works for pilot.

Comment on lines +33 to +45
(if jwt.nil?
if api_key.nil?
if username.nil? || escaped_password.nil?
nil
else
"-u #{username.shellescape} -p #{escaped_password}"
end
else
"-apiIssuer #{api_key[:issuer_id]} -apiKey #{api_key[:key_id]}"
end
else
"-jwt #{jwt}"
end),
Copy link
Contributor

@AliSoftware AliSoftware Mar 12, 2025

Choose a reason for hiding this comment

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

Same DRY-ing suggestion as above.

Actually, one could even consider extracting the credential_params variable I suggested in comment above into a dedicated helper method on the Transporter class (just like the prepare one you added), so we can then add dedicated tests for that helper function that would cover all the different cases (with or without JWT, with or without API key, with or without username/password, with a mix of all those different options, …).

e.g. something probably like this (not tested, might contain typos):

describe '#credential_params' do
  shared_examples 'credential_params' do |jwt: nil, username: nil, password: nil, api_key: nil, expected:|
    input_params = { jwt: jwt, username: username, password: password, api_key: api_key }
    context "when passing #{input_params.compact.keys.join(', ')" do
      it "calls the command line with #{expected}" do
        cmd_line_param = transporter.credential_params(**input_params)
        expect(cmd_line_params).to eq(expected)
      end
    end
  end

  # no command line param if nothing provided
  it_behaves_like 'credential_params',
    expected: []

  # if we only provide JWT and nothing else
  it_behaves_like 'credential_params',
    jwt: jwt,
    expected: ['-jwt', jwt]

  # if we only provide username and password
  it_behaves_like 'credential_params',
    username: email, password: password,
    expected: ['-u', email.shellescape, '-p', password.shellescape]

  # if we only provide API key
  it_behaves_like 'credential_params',
    api_key: api_key,
    expected: ['-apiIssuer', api_key[:issuer_id], '-apiKey', api_key[:key_id]]

  # If we provide both the username and password AND an api_key, api_key takes precedence
  it_behaves_like 'credential_params',
    username: email, password: password,
    api_key: api_key,
    expected: ['-apiIssuer', api_key[:issuer_id], '-apiKey', api_key[:key_id]]

  # … add all other combinations here
end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not a bad idea 👌🏻

made a new method in TransporterExecutor called build_credential_params, overwrote that in all subclasses and added tests as you suggested.

@bcg00ding
Copy link

@ccaruceru I was able to validate your work in a Docker container and uploaded an ipa to TestFlight using Debian. Any chance you could make the suggested changes?

@ccaruceru
Copy link
Contributor Author

I was able to validate your work in a Docker container and uploaded an ipa to TestFlight using Debian. Any chance you could make the suggested changes?

Sorry it took so long for me to reply. I was away for quite a while and now I'm back. I'll check the PR these days and address any comments.

[
'"' + Helper.transporter_path + '"',
'-m provider',
("-u \"#{username.shellescape}\"" unless use_jwt),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the new build_credential_params doesn't surround the username with "" anymore.

this is the only place in ShellScriptTransporterExecutor that uses quotes and I tried tracing this back to #13174, but couldn't find a reason to keep them nor a reason why this should be different from the other commands

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I agree that I wouldn't expect the username to be both shellescaped and surrounded by quotes, not sure why it was before, probably a bug that got unnoticed. 👍

@ccaruceru
Copy link
Contributor Author

@AliSoftware this is ready for a second review 🔎

Copy link
Contributor

@AliSoftware AliSoftware left a comment

Choose a reason for hiding this comment

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

Just left a question for the base class, but otherwise LGTM 👍

Comment on lines 318 to 319
("-u #{username.shellescape}" if api_key.nil?),
("-p #{password.shellescape}" if api_key.nil?),
Copy link
Contributor

Choose a reason for hiding this comment

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

I know that's just you moving the existing code from former lines 283–286 into here without changes… but doesn't that logic mean that it'll add -u nil if api_key is nil… even jwt isn't nil? 💭

Wouldn't it make more sense to add those only if username.nil? / password.nil? respectively instead? 🤔

Copy link
Contributor Author

@ccaruceru ccaruceru Apr 9, 2025

Choose a reason for hiding this comment

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

I see. It makes more sense to check for username and password rather than presence/absence of api_key. I tried to look at #20631 when this was introduced, but couldn't find a reason why should be like that 🤷🏻‍♂️


I also observed that we're always returning one combination of user/pass, jwt, or api key, and it would be more appropriate to change build_credential_params to return a string with the right credential combination instead of an array.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@AliSoftware can you have one more look?

[
'"' + Helper.transporter_path + '"',
'-m provider',
("-u \"#{username.shellescape}\"" unless use_jwt),
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I agree that I wouldn't expect the username to be both shellescaped and surrounded by quotes, not sure why it was before, probably a bug that got unnoticed. 👍

@ccaruceru
Copy link
Contributor Author

@AliSoftware is this okay to be merged?

@AliSoftware AliSoftware merged commit 6dc02f6 into fastlane:master Apr 17, 2025
3 checks passed
@ccaruceru ccaruceru deleted the feature/update-linux-itms-transporter branch April 17, 2025 22:21
@lucasfernog-crabnebula
Copy link

btw we should update the docs https://docs.fastlane.tools/actions/pilot/#pilot

@ccaruceru
Copy link
Contributor Author

btw we should update the docs https://docs.fastlane.tools/actions/pilot/#pilot

docs should be updated with fastlane/docs#1282

@lucasfernog
Copy link

anyone else facing issues with this recently? seems like the transporter is stuck uploading the IPA to TestFlight (i'm checking if it's a transporter regression.. probably the case)

@lucasfernog
Copy link

well downgrading the iTunesTransporter to 3.4.1 indeed fixes the problem..

@SomeRandomiOSDev
Copy link

The latest version of the transporter works for me. I’m using a Rocky Linux 9 docker image with Ruby, Fastlane, and the transporter installed

@00061172
Copy link

The latest version of the transporter works for me. I’m using a Rocky Linux 9 docker image with Ruby, Fastlane, and the transporter installed

I tried to check feature in Rocky Linux 9, Fastlane version 2.228.0 and iTMSTransporter version 4.1.
And I got the following error:
`[11:47:16]: Get started using a Gemfile for fastlane https://docs.fastlane.tools/getting-started/ios/setup/#use-a-gemfile
[11:47:17]: Driving the lane 'ios deliver_testflight' 🚀
[11:47:17]: ---------------------------------------
[11:47:17]: --- Step: app_store_connect_api_key ---
[11:47:17]: ---------------------------------------
[11:47:17]: ----------------------------------
[11:47:17]: --- Step: upload_to_testflight ---
[11:47:17]: ----------------------------------
[11:47:17]: Creating authorization token for App Store Connect API
[11:47:18]: Ready to upload new build to TestFlight (App: 1500868687)...
[11:47:21]: Going to upload updated app to App Store Connect
[11:47:21]: This might take a few minutes. Please don't interrupt the script.
11:47:30: Transporter transfer failed.
11:47:30:
11:47:30: Unable to perform software analysis on Linux. Export an AppStoreInfo.plist from Xcode, and use the -assetDescription option.
11:47:30: [iTMSTransporter] [2025-09-10 11:47:30 UTC]

DEBUG: SMART-CLIENT: getCurrent(); iterator was null, setting it

11:47:30: [iTMSTransporter] [2025-09-10 11:47:30 UTC]

DEBUG: SMART-CLIENT: getCurrent(); setting current data center to: contentdelivery01.itunes.apple.com

11:47:30: [iTMSTransporter] [2025-09-10 11:47:30 UTC]

INFO: Setting transport log file: ac0cdb7b-e347-4bb0-b0c4-665a90d480dc15540698444925637104.tx.log

11:47:30: [iTMSTransporter] [2025-09-10 11:47:30 UTC]

INFO: Configuring the Software Uploader...

11:47:30: [iTMSTransporter] [2025-09-10 11:47:30 UTC]

INFO: Performing analysis...

11:47:30: [iTMSTransporter] [2025-09-10 11:47:30 UTC]

ERROR: Unable to perform software analysis on Linux. Export an AppStoreInfo.plist from Xcode, and use the -assetDescription option.

11:47:30: [iTMSTransporter] Package Summary:

11:47:30: [iTMSTransporter] 1 package(s) were not uploaded because they had problems:

11:47:30: [iTMSTransporter] /tmp/d20250910-47-xuzuao/1500868687-3c0c56f4-93f3-4058-85e2-2dd348c06739.itmsp/bbd528aa542af044f34a659fcb5a638406ec1052eff25f206da5be3ceca44551.ipa - Error Messages:

11:47:30: [iTMSTransporter] Unable to perform software analysis on Linux. Export an AppStoreInfo.plist from Xcode, and use the -assetDescription option.

11:47:30: [iTMSTransporter] [2025-09-10 11:47:30 UTC]

DBG-X: Returning 1

11:47:30: iTunes Transporter output above ^
11:47:30: Unable to perform software analysis on Linux. Export an AppStoreInfo.plist from Xcode, and use the -assetDescription option.
Return status of iTunes Transporter was 1: Unable to perform software analysis on Linux. Export an AppStoreInfo.plist from Xcode, and use the -assetDescription option.
The call to the iTMSTransporter completed with a non-zero exit status: 1. This indicates a failure.
11:47:30: Password contains special characters, which may not be handled properly by iTMSTransporter. If you experience problems uploading to App Store Connect, please consider changing your password to something with only alphanumeric characters.
11:47:30: Could not download/upload from App Store Connect! It's probably related to your password or your internet connection.
+----------------------------------------+
| Lane Context |
+---------------+------------------------+
| PLATFORM_NAME | ios |
| LANE_NAME | ios deliver_testflight |
+---------------+------------------------+
11:47:30: Called from Fastfile at line 94
11:47:30: [11:47:30]: 92: key_filepath: "ApiKey_0UA7JW0J8OG4.p8" [11:47:30]: 93: ) [11:47:30]: => 94: upload_to_testflight( [11:47:30]: 95: #api_key_path: "api-key.json", [11:47:30]: 96: api_key: api_key, [11:47:30]:
11:47:30: Error uploading ipa file:`.

Please, can you say me what's may be wrong?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants