Upgrade to Pro — share decks privately, control downloads, hide ads and more …

iOSアプリ開発における自動化ノウハウ

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

 iOSアプリ開発における自動化ノウハウ

Avatar for Kishikawa Katsumi

Kishikawa Katsumi

March 01, 2015
Tweet

More Decks by Kishikawa Katsumi

Other Decks in Technology

Transcript

  1. มߋ มߋ มߋ มߋ มߋ W ϦϦʔε ख࡞ۀͷʢ͚ͩͲΑ͋͘ΔʣϦϦʔεϓϩηε HJUQVMM "MSFBEZVQUPEBUF

    2. खݩͷ؀ڥΛ࠷৽ʹߋ৽ͯ͠ 1. ϦϞʔτϦϙδτϦͷߋ৽ΛࢭΊͯ 3. όʔδϣϯ൪߸Λߋ৽ͯ͠
  2. มߋ มߋ มߋ มߋ มߋ W ਃ੥ ख࡞ۀͷʢ͚ͩͲΑ͋͘ΔʣϦϦʔεϓϩηε HJUQVMM 6QEBUJOHFFCE

    FSSPS:PVSMPDBMDIBOHFTUPUIFGPMMPXJOHpMFTXPVMECFPWFSXSJUUFOCZNFSHF 6CJSFHJ"QQ%FMFHBUFN 1MFBTF DPNNJUZPVSDIBOHFTPSTUBTIUIFNCFGPSFZPVDBONFSHF "CPSUJOH 2. खݩͷ؀ڥΛ࠷৽ʹߋ৽ͯ͠ 1. ϦϞʔτϦϙδτϦͷߋ৽ΛࢭΊͯ ߋ৽ʹࣦഊͨ͠ͷʹؾ͔ͮͳ͔ͬͨ
  3. มߋ มߋ มߋ มߋ มߋ W ਃ੥ ख࡞ۀͷʢ͚ͩͲΑ͋͘ΔʣϦϦʔεϓϩηε HJUGFUDI 

    2. खݩͷ؀ڥΛ࠷৽ʹߋ৽ͯ͠ 1. ϦϞʔτϦϙδτϦͷߋ৽ΛࢭΊͯ HJUGFUDI͚ͩͯ͠NFSHFΛ๨Εͨ
  4. มߋ มߋ มߋ มߋ มߋ W ਃ੥ ख࡞ۀͷʢ͚ͩͲΑ͋͘ΔʣϦϦʔεϓϩηε HJUQVMM "MSFBEZVQUPEBUF

    2. खݩͷ؀ڥΛ࠷৽ʹߋ৽ͯ͠ 1. ϦϞʔτϦϙδτϦͷߋ৽ΛࢭΊͯ ࣮͸खݩͷํ͕ϦϞʔτΑΓਐΜͰ͍ͨ
  5. • खݩͷ؀ڥ͕ϦϞʔτͱҟͳ͍ͬͯͨ • Ϗϧυߏ੒͕ҧ͍ͬͯͨ • Provisioning Profile͕ҧ͏ • όʔδϣϯ൪߸Λ͚ͭؒҧ͑ͨ •

    ϦδΣΫτ͞Εͨ৔߹ɺͲ͜ʹ໭Δ͔ • etc… ख࡞ۀͷϦϦʔεϓϩηεʹ͓͚Δࣦഊྫ
  6. w ʮࣗಈԽͰ͖Δঢ়ଶʯΛอͭ͜ͱ͕ॏཁ w ʮࣗಈԽͰ͖Δঢ়ଶʯͱ͸λεΫ͕γϯϓϧʹ෼ׂ ͞ΕͯػցͰ΋࣮ߦՄೳͰ͋Δͱ͍͏͜ͱ w ػցͰ࣮ߦͰ͖ΔͳΒɺ܁Γฦͨ͠Γɺه࿥΍࿈ܞ ͕༰қͰ͋Δ w ୭͕ݟͯ΋ঢ়ଶ͕೺ѲͰ͖ΔΑ͏ʹͳΔ

    w ࣗಈԽ͕໨తͰ͸ͳͯ͘ɺࣗಈԽͰ͖͍ͯΔͳΒʮࣗ ಈԽͰ͖Δঢ়ଶʯ͕ҡ࣋Ͱ͖͍ͯΔͱ͍͏͜ͱ w ࣗಈԽʹΑͬͯʮࣗಈԽͰ͖Δঢ়ଶʯ͕ҡ࣋͞Εͯ Δ͜ͱΛอূ͢Δ
  7. • มߋ؅ཧ • Github • Pull Request • ίʔυϨϏϡʔ •

    ϥΠϒϥϦ؅ཧ • ࣗಈϏϧυ • ςετ • ςετͷࣗಈԽ • ܧଓతΠϯςάϨʔγϣϯʢCIʣ • ίʔυΧόϨοδ ࣗಈԽ΁ͷऔΓ૊Έ
  8. • ෼ࢄܕόʔδϣϯ؅ཧγεςϜ • Git → Github, Bitbucket • Mercurial →

    Bitbucket มߋ؅ཧ Github͔BitbucketΛҰॹʹ࢖͓͏ʂ
  9. source 'https://github.com/CocoaPods/Specs.git' source 'https://[email protected]/ubiregiinc/Specs.git' platform :ios, '7.0' inhibit_all_warnings! pod 'UUIDShortener'

    pod 'JLRoutes' pod 'NLCoreData' pod 'AFNetworking' pod 'AFNetworkActivityLogger' pod 'SDWebImage' pod 'PEPhotoCropEditor' pod 'UICKeyChainStore' pod 'GCDWebServer', '~> 1.4' pod 'FXImageView' pod 'FastCoding' pod 'SSZipArchive' pod 'Helpshift' pod 'CrittercismSDK' pod 'Aspects' pod 'GoogleAnalytics-iOS-SDK' target :Ubiregi2Tests do pod 'OCMock' pod 'OCHamcrest' pod 'OHHTTPStubs' end
  10. PODS: - AFNetworkActivityLogger (2.0.3): - AFNetworking/NSURLConnection (~> 2.0) - AFNetworking/NSURLSession

    (~> 2.0) - AFNetworking (2.5.0): - AFNetworking/NSURLConnection (= 2.5.0) - AFNetworking/NSURLSession (= 2.5.0) - AFNetworking/Reachability (= 2.5.0) - AFNetworking/Security (= 2.5.0) - AFNetworking/Serialization (= 2.5.0) - AFNetworking/UIKit (= 2.5.0) - Aspects (1.4.1) - CoineyKit-iOS (0.0.7) - CrittercismSDK (5.0.7) - FastCoding (3.2) - FXImageView (1.3.5) - GCDWebServer (1.4) - GoogleAnalytics-iOS-SDK (3.10): - GoogleAnalytics-iOS-SDK/Core (= 3.10) - GoogleAnalytics-iOS-SDK/Core (3.10) - Helpshift (4.9.1) - JLRoutes (1.5.1) - M13BadgeView (1.0.4) - NLCoreData (0.5.3) - OCHamcrest (4.1.1) - OCMock (3.1.2) - OHHTTPStubs (3.1.10): - OHHTTPStubs/Core (= 3.1.10) - OHHTTPStubs/Core (3.1.10) - PEPhotoCropEditor (1.3.1) - SDWebImage (3.7.1): - SDWebImage/Core (= 3.7.1) - SDWebImage/Core (3.7.1) - SEJSONViewController (0.2.0) - SSZipArchive (0.3.2) - UICKeyChainStore (2.0.2) - UUIDShortener (1.0.1) - YLProgressBar (3.5.1)
  11. • WebαʔϏε • Travis CI • Circle CI • Greenhouse

    CI • ιϑτ΢ΣΞ • Jenkins มߋ؅ཧ
  12. xcodebuild test -workspace Ubiregi2.xcworkspace -scheme Ubiregi2-Release -sdk iphonesimulator -configuration Release

    -derivedDataPath build -destination name=iPad 2,OS=7.1 -destination name=iPad Air,OS=8.1 CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO GCC_SYMBOLS_PRIVATE_EXTERN=NO 1. γϛϡϨʔλ༻ʹϏϧυͯ͠ɺςετΛ࣮ߦ ίϚϯυϥΠϯ • Ϗϧυߏ੒(Debug/Release)͸Ҿ਺Ͱࢦఆ • destinationύϥϝʔλͰ࣮ߦ؀ڥΛࢦఆͰ͖Δ • GCC_SYMBOLS_PRIVATE_EXTERNύϥϝʔλΛҾ਺Ͱ্ॻ͖
  13. $ xcodebuild build -workspace Ubiregi2.xcworkspace -scheme Ubiregi2-Release -configuration Release -derivedDataPath

    "build" CONFIGURATION_TEMP_DIR="build/temp" CODE_SIGN_IDENTITY="iPhone Distribution: Ubiregi Inc. (A1234567XZ)" 2. σόΠε༻ʹϏϧυ͢Δɻ ίϚϯυϥΠϯ • ϓϩϏδϣχϯάϓϩϑΝΠϧ͸Ҿ਺Ͱࢦఆ ʢෳ਺ͷλʔήοτ͕͋Δ৔߹͸.xcodeprojͰࢦఆ͢Δʣ
  14. $ xcodebuild archive -workspace Ubiregi2.xcworkspace -scheme Ubiregi2-Release -configuration Release -derivedDataPath

    "build" CONFIGURATION_TEMP_DIR="build/temp" CODE_SIGN_IDENTITY="iPhone Distribution: Ubiregi Inc. (A1234567XZ)" -archivePath "build/Ubiregi2-Release" 3. ϦϦʔεߏ੒ͰϏϧυͯ͠ɺ.xcarchiveʢਃ੥༻ͷΞʔΧΠϒʣΛ࡞Δ ίϚϯυϥΠϯ • ϓϩϏδϣχϯάϓϩϑΝΠϧ͸Ҿ਺Ͱࢦఆ ʢෳ਺ͷλʔήοτ͕͋Δ৔߹͸.xcodeprojͰࢦఆ͢Δʣ
  15. $ xcodebuild -exportArchive -archivePath "build/Ubiregi2-Release.xcarchive" -exportFormat "IPA" -exportPath "build/Ubiregi2-AdHoc.ipa" -exportProvisioningProfile

    "Ubiregi AdHoc" 4. ਃ੥༻ͷϏϧυ͔ΒɺϓϩϏδϣχϯάϓϩϑΝΠϧ͚ͩม͑ͯ ݕূ༻ͷϕʔλ൛ͷ.ipaΛ࡞੒͢Δ ʢਃ੥ͨ͠΋ͷͱॺ໊Ҏ֎͸શ͘ಉ͡όΠφϦͰݕূͰ͖Δʣ ίϚϯυϥΠϯ
  16. $ xcodebuild -exportArchive -archivePath "build/Ubiregi2-Release.xcarchive" -exportFormat "IPA" -exportPath "build/Ubiregi2-Release-AppStore.ipa" -exportProvisioningProfile

    "Ubiregi App Store" 5. ਃ੥༻ͷϏϧυ͔ΒɺApplication LoaderͰΞοϓϩʔυ͢ΔͨΊʹ.ipaΛ࡞੒͢Δ ίϚϯυϥΠϯ
  17. def build_options(opt = {configuration: "Release"}) options = { sdk: "iphoneos",

    workspace: "Ubiregi2-Release.xcworkspace", scheme: "Ubiregi2-Release", }.merge opt join_options(options: options, prefix: "-", separator: " ") end def build_settings(configuration: "Release") settings = { CONFIGURATION_BUILD_DIR: "build", CONFIGURATION_TEMP_DIR: "build/temp", CODE_SIGN_IDENTITY: "iPhone Distribution: Ubiregi Inc. (Y7522692LT)", } settings = join_options(options: settings, prefix: "", separator: "=") end def test(configuration: "Release") options = build_options(sdk: "iphonesimulator", configuration: configuration) options << DESTINATIONS.map { |destination| %(-destination "#{destination}") }.join(" ") sh "xcodebuild #{options} test | xcpretty -c" end def build_xcarchive(configuration: "Release") options = build_options(configuration: configuration) settings = build_settings(configuration: configuration) sh "xcodebuild #{options} #{settings} archive -archivePath #{ARCHIVE_FILE} | xcpretty -c" end def export_ipa sh "xcodebuild -exportArchive -exportFormat IPA -archivePath #{ARCHIVE_FILE} -exportPath #{IPA_FILE} | xcpretty -c" end ֤ίϚϯυΛλεΫʹ͢Δ Rake
  18. release_date = DateTime.now.strftime("%Y/%m/%d %H:%M:%S") release_notes = "Build: #{InfoPlist.marketing_and_build_version} \nUploaded: #{release_date}"

    fields = { file: "@#{IPA_FILE}", api_token: API_TOKEN, team_token: TEAM_TOKEN, notes: release_notes, notify: true, distribution_lists: DISTRIBUTION_LISTS } fields = join_option(options: fields, prefix: "-F ", seperator: "=") sh "curl -sL -w \"%{http_code} %{url_effective}\\n\" http:// testflightapp.com/api/builds.json #{fields} -o /dev/null ֤ίϚϯυΛλεΫʹ͢Δ Rake
  19. release_date = DateTime.now.strftime("%Y/%m/%d %H:%M:%S") release_notes = "Build: #{InfoPlist.marketing_and_build_version} \nUploaded: #{release_date}"

    fields = { file: "@#{IPA_FILE}", api_token: API_TOKEN, team_token: TEAM_TOKEN, notes: release_notes, notify: true, distribution_lists: DISTRIBUTION_LISTS } fields = join_option(options: fields, prefix: "-F ", seperator: "=") sh "curl -sL -w \"%{http_code} %{url_effective}\\n\" http:// testflightapp.com/api/builds.json #{fields} -o /dev/null ֤ίϚϯυΛλεΫʹ͢Δ Rake
  20. XCJobs::Test.new do |t| t.workspace = 'Ubiregi2' t.scheme = 'Ubiregi2-Release' t.configuration

    = 'Release' t.build_dir = 'build' t.add_destination('name=iPad 2,OS=7.1') t.add_destination('name=iPad Air,OS=8.1') end xcodebuild test -workspace Ubiregi2.xcworkspace -scheme Ubiregi2-Release -sdk iphonesimulator -configuration Release -derivedDataPath build -destination name=iPad 2,OS=7.1 -destination name=iPad Air,OS=8.1 CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO GCC_SYMBOLS_PRIVATE_EXTERN=NO $ rake test 1. γϛϡϨʔλ༻ʹϏϧυͯ͠ɺςετΛ࣮ߦ
  21. XCJobs::Archive.new do |t| t.workspace = APP_NAME t.scheme = SCHEME t.configuration

    = configuration t.signing_identity = signing_identity t.build_dir = BUILD_DIR t.formatter = 'xcpretty -c' end xcodebuild archive -workspace Ubiregi2.xcworkspace -scheme Ubiregi2-Release -configuration Release -derivedDataPath "build" CONFIGURATION_TEMP_DIR="build/temp" CODE_SIGN_IDENTITY="iPhone Distribution: Ubiregi Inc. (A1234567XZ)" -archivePath "build/Ubiregi2-Release" $ rake build:archive 3. ϦϦʔεߏ੒ͰϏϧυͯ͠ɺ.xcarchiveʢਃ੥༻ͷΞʔΧΠϒʣΛ࡞Δ
  22. XCJobs::Export.new do |t| t.archive_path = File.join('build', 'Ubiregi2.xcarchive') t.export_path = File.join('build',

    'Ubiregi2.ipa') t.export_provisioning_profile = 'Ubiregi_Ad_Hoc.mobileprovision' t.formatter = 'xcpretty -c' end xcodebuild -exportArchive -archivePath "build/Ubiregi2.xcarchive" -exportFormat "IPA" -exportPath "build/Ubiregi2-AdHoc.ipa" -exportProvisioningProfile "Ubiregi AdHoc" $ rake build:export 4. ਃ੥༻ͷϏϧυ͔ΒɺϓϩϏδϣχϯάϓϩϑΝΠϧ͚ͩม͑ͯ ݕূ༻ͷϕʔλ൛ͷ.ipaΛ࡞੒͢Δ
  23. language: objective-c install: - bundle install - bundle exec pod

    install script: - bundle exec rake test profiles:install certificates:install version:set_build_version build:archive build:export distribute:testflight CIͱ؆୯ʹ࿈ܞͰ͖Δ
  24. XCJobs::Test.new do |t| t.workspace = 'UCZProgressView' t.scheme = 'UCZProgressView' t.configuration

    = 'Release' t.build_dir = 'build' t.coverage = true t.formatter = 'xcpretty -c' end XCJobs::Coverage::Coveralls.new() do |t| t.add_extension('.m') t.add_exclude('Example') end ؆୯ͳϓϩδΣΫτͷ৔߹
  25. language: objective-c install: - bundle install script: - bundle exec

    rake test coverage:coveralls ؆୯ͳϓϩδΣΫτͷ৔߹
  26. Ϣχοτςετ /* "vat" : 5 "price" : "2000.0", "price_type" :

    "intax" */ UBCheckoutItem *checkoutItem = nil; for (checkoutItem in self.checkoutItems) { if (checkoutItem.checkoutMenuItem.isTaxIncluded) { break; } } checkoutItem.discount = [NSDecimalNumber decimalNumberWithString:@"500"]; checkoutItem.count = @(1); XCTAssertEqualObjects(checkoutItem.apiSalesNet, [NSDecimalNumber decimalNumberWithString:@"1905"]); XCTAssertEqualObjects(checkoutItem.apiSalesTax, [NSDecimalNumber decimalNumberWithString:@"95"]); XCTAssertEqualObjects(checkoutItem.apiDiscountNet, [NSDecimalNumber decimalNumberWithString:@"476"]); XCTAssertEqualObjects(checkoutItem.apiDiscountTax, [NSDecimalNumber decimalNumberWithString:@"24"]); XCTAssertEqualObjects(checkoutItem.discountRatio, [NSDecimalNumber decimalNumberWithString:@"25"]); checkoutItem.count = @(2); XCTAssertEqualObjects(checkoutItem.apiSalesNet, [NSDecimalNumber decimalNumberWithString:@"3810"]); XCTAssertEqualObjects(checkoutItem.apiSalesTax, [NSDecimalNumber decimalNumberWithString:@"190"]); XCTAssertEqualObjects(checkoutItem.apiDiscountNet, [NSDecimalNumber decimalNumberWithString:@"476"]); XCTAssertEqualObjects(checkoutItem.apiDiscountTax, [NSDecimalNumber decimalNumberWithString:@"24"]); ֹۚͷςετ
  27. Ϣχοτςετ NSManagedObjectContext *context = [NSManagedObjectContext mainContext]; NSMutableOrderedSet *checkoutItems = [[NSMutableOrderedSet

    alloc] init]; UBCheckoutItem *item; item = [UBCheckoutItem checkoutItemWithMenuItem:self.menuItems[@"intaxItem"]]; item.count = @(1); item.discount = [NSDecimalNumber decimalNumberWithString:@"500"]; [checkoutItems addObject:item]; item = [UBCheckoutItem checkoutItemWithMenuItem:self.menuItems[@"outtaxItem"]]; item.count = @(1); item.discount = [NSDecimalNumber decimalNumberWithString:@"190"]; [checkoutItems addObject:item]; item = [UBCheckoutItem checkoutItemWithMenuItem:self.menuItems[@"nosalesItem"]]; item.count = @(1); item.discount = [NSDecimalNumber decimalNumberWithString:@"-500"]; [checkoutItems addObject:item]; item = [UBCheckoutItem checkoutItemWithMenuItem:self.menuItems[@"percentageItem"]]; item.count = @(1); [checkoutItems addObject:item]; UBCheckout *checkout = [UBCheckout insertInContext:context]; checkout.checkoutItems = checkoutItems; XCTAssertEqualObjects(checkout.total, [NSDecimalNumber decimalNumberWithString:@"3737"], ɹɹɹɹɹɹɹɹɹɹɹɹɹ@"Ϣʔβʔʹ੥ٻ͞ΕΔֹۚ"); XCTAssertEqualObjects(checkout.tax, [NSDecimalNumber decimalNumberWithString:@“192"], ɹɹɹɹɹɹɹɹɹɹɹɹɹ @"಺ফඅ੫ֹ"); ֹۚͷςετ
  28. Ϣχοτςετ NSDictionary *response = [UBTestUtils JSONObjectWithPath:self.dataPath]; [UBTestUtils stubWithJSONObject:response path:self.endpoint]; NSManagedObjectContext

    *context = [NSManagedObjectContext mainContext]; UBAccount *account = [UBAccount currentAccountInContext:context]; UBAccountSynchronizer *synchronizer = [[UBAccountSynchronizer alloc] initWithAccountID:account.identifier]; XCTestExpectation *expectation = [self expectationWithDescription:@"finish synchronization"]; synchronizer.completionHandler = ^(NSError *error) { assertThat(error, nilValue()); [expectation fulfill]; }; [synchronizer synchronize]; [self waitForExpectationsWithTimeout:10.0 handler:nil]; [context refreshObject:account mergeChanges:YES]; NSDictionary *result = account.dictionaryRepresentation; NSDictionary *expected = response[@"account"]; XCTAssertEqualObjects(result[@"id"], expected[@"id"]); XCTAssertEqualObjects(result[@"login"], expected[@"login"]); XCTAssertEqualObjects(result[@"name"], expected[@"name"]); XCTAssertEqualObjects(result[@"email"], expected[@"email"]); ௨৴ͷςετ
  29. Ϣχοτςετ + (id)stubWithJSONObject:(id)JSONObject statusCode:(int)statusCode path: (NSString *)path { id<OHHTTPStubsDescriptor> stub

    = nil; stub = [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { return [request.URL.path hasPrefix:path]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { return [[OHHTTPStubsResponse responseWithJSONObject:JSONObject statusCode:statusCode headers:nil] requestTime:1.0 responseTime:1.0]; }]; return stub; } ௨৴Λελϒʹ͢Δ
  30. • ςετ͸Πϯϑϥ • ؀ڥΛ੔͑Δ͜ͱ͕େࣄ • جຊతʹ໘౗ͳͷͰࣗಈతʹ࣮ߦ͞ΕΔ࢓૊ ΈΛ࡞Δ • ఆظతʹ࣮ߦ͢Δ •

    ॻ͖΍͍͢ͱ͜Ζ͔Βॻ͘ • େࣄͳͱ͜Ζ͔Βॻ͘ • ॻ͖΍͍͢Α͏ͳઃܭΛ͜͜Ζ͕͚Δ Ϣχοτςετ
  31. XCJobs::Test.new do |t| t.workspace = 'Ubiregi2' t.scheme = ‘Ubiregi2-Release' t.configuration

    = 'Release' t.build_dir = 'build' t.add_destination('name=iPad 2,OS=7.1') t.add_destination('name=iPad Air,OS=8.1') t.coverage = true t.formatter = 'xcpretty -c' end XCJobs::Coverage::Coveralls.new() do |t| t.service_name = 'travis-pro' t.repo_token = ‘xxx...’ t.add_extension('.m') t.add_exclude('Pods') t.add_exclude('Ubiregi2Tests') t.add_exclude_pattern('.*ViewController\.[hm]') end ίʔυΧόϨοδ XCJobsͰઃఆ͢Δ৔߹