class Spaceship::Tunes::BuildTrain

A build train is all builds for a given version number with different build numbers
Represents a build train of builds from iTunes Connect

def all(application, app_id, platform: nil)

Parameters:
  • app_id (String) -- The unique Apple ID of this app
  • application (Spaceship::Tunes::Application) -- The app this train is for
def all(application, app_id, platform: nil)
  trains = []
  trains += client.build_trains(app_id, 'internal', platform: platform)['trains']
  trains += client.build_trains(app_id, 'external', platform: platform)['trains']
  result = {}
  trains.each do |attrs|
    attrs[:application] = application
    current = self.factory(attrs)
    if (!platform.nil? && current.platform == platform) || platform.nil?
      result[current.version_string] = current
    end
  end
  result
end

def factory(attrs)

This is used to create a new object based on the server response.
Create a new object based on a hash.
def factory(attrs)
  self.new(attrs)
end

def latest_build

Returns:
  • (Spaceship::Tunes::Build) - The latest build for this train, sorted by upload time.
def latest_build
  @builds.max_by(&:upload_date)
end

def setup

Setup all the builds and processing builds
def setup
  super
  @builds = (self.raw_data['builds'] || []).collect do |attrs|
    attrs[:build_train] = self
    Tunes::Build.factory(attrs)
  end
  @invalid_builds = @builds.select do |build|
    build.processing_state == 'processingFailed' || build.processing_state == 'invalidBinary'
  end
  # This step may not be necessary anymore - it seems as if every processing build will be caught by the
  # @builds.each below, but not every processing build makes it to buildsInProcessing, so this is redundant
  @processing_builds = (self.raw_data['buildsInProcessing'] || []).collect do |attrs|
    attrs[:build_train] = self
    Tunes::Build.factory(attrs)
  end
  # since buildsInProcessing appears empty, fallback to also including processing state from @builds
  @builds.each do |build|
    # What combination of attributes constitutes which state is pretty complicated. The table below summarizes
    # what I've observed, but there's no reason to believe there aren't more states I just haven't seen yet.
    # The column headers are qualitative states of a given build, and the first column is the observed attributes
    # of that build.
    # NOTE: Some of the builds in the build_trains.json fixture do not follow these rules. I don't know if that is
    # because those examples are older, and the iTC API has changed, or if their format is still a possibility.
    # The second part of the OR clause in the line below exists so that those suspicious examples continue to be
    # accepted for unit tests.
    # +---------------------+-------------------+-------------------+-----------------+--------------------+---------+
    # |                     | just after upload | normal processing | invalid binary  | processing failed  | success |
    # +---------------------+-------------------+-------------------+-----------------+--------------------+---------+
    # |  build.processing = | true              | true              | true            | true               | false   |
    # |       build.valid = | false             | true              | false           | true               | true    |
    # | .processing_state = | "processing"      | "processing"      | "invalidBinary" | "processingFailed" | nil     |
    # +---------------------+-------------------+-------------------+-----------------+--------------------+---------+
    if build.processing_state == 'processing' || (build.processing && build.processing_state != 'invalidBinary' && build.processing_state != 'processingFailed')
      @processing_builds << build
    end
  end
  self.version_set = self.application.version_set_for_platform(self.platform)
end

def update_testing_status!(new_value, testing_type, build = nil)

Parameters:
  • internal (testing_type) -- or external
def update_testing_status!(new_value, testing_type, build = nil)
  data = client.build_trains(self.application.apple_id, testing_type, platform: self.application.platform)
  build ||= latest_build if testing_type == 'external'
  testing_key = "#{testing_type}Testing"
  # Delete the irrelevant trains and update the relevant one to enable testing
  data['trains'].delete_if do |train|
    if train['versionString'] != version_string
      true
    else
      train[testing_key]['value'] = new_value
      # also update the builds
      train['builds'].delete_if do |b|
        if b[testing_key].nil?
          true
        elsif build && b["buildVersion"] == build.build_version
          b[testing_key]['value'] = new_value
          false
        elsif b[testing_key]['value'] == true
          b[testing_key]['value'] = false
          false
        else
          true
        end
      end
      false
    end
  end
  begin
    result = client.update_build_trains!(application.apple_id, testing_type, data)
  rescue Spaceship::TunesClient::ITunesConnectError => ex
    if ex.to_s.include?("You must provide an answer for this question")
      # This is a very common error message that's raised by TestFlight
      # We want to show a nicer error message with instructions on how
      # to resolve the underlying issue
      # https://github.com/fastlane/fastlane/issues/1873
      # https://github.com/fastlane/fastlane/issues/4002
      error_message = [""] # to have a nice new-line in the beginning
      error_message << "TestFlight requires you to provide the answer to the encryption question"
      error_message << "to provide the reply, please add the following to your Info.plist file"
      error_message << ""
      error_message << "<key>ITSAppUsesNonExemptEncryption</key><false/>"
      error_message << ""
      error_message << "Afterwards re-build your app and try again"
      error_message << "iTunes Connect reported: '#{ex}'"
      raise error_message.join("\n")
    else
      raise ex
    end
  end
  self.internal_testing_enabled = new_value if testing_type == 'internal'
  self.external_testing_enabled = new_value if testing_type == 'external'
  result
end