iOS Dev Workflow Automation for note

26173c2f52b5aa5d06e6806c7a9478d3?s=47 laprasDrum
September 21, 2020

iOS Dev Workflow Automation for note

iOSDC Japan 2020 で登壇しました。

「それ、自動化できますよ」: note を支えるワークフロー大全
https://fortee.jp/iosdc-japan-2020/proposal/a6fcae29-405e-4923-aa45-8132a2444e60

補足版を技術書典で販売しています。
よければ見てくれるとうれしいです。

Zapier ではじめる iOS 開発ワークフロー自動化
https://techbookfest.org/product/5017415553384448

26173c2f52b5aa5d06e6806c7a9478d3?s=128

laprasDrum

September 21, 2020
Tweet

Transcript

  1. laprasDrum (Β΀) note inc. ʮͦΕɺࣗಈԽͰ͖·͢Αʯ note Λࢧ͑ΔϫʔΫϑϩʔେશ iOSDC JAPAN 2020

  2. 2018/5 2019 2020/5 1ਓ 2ਓ 3ਓ ೖࣾ 2ਓೖࣾ

  3. 2018/5 2019 2020/5 1ਓ 2ਓ 3ਓ ೖࣾ 2ਓೖࣾ ࣗ෼Λॿ͚ΔͨΊ ະདྷͷಉ྅Λॿ͚ΔͨΊ

  4. iOS ։ൃʹ͓͚ΔࣗಈԽʁ

  5. None
  6. None
  7. None
  8. None
  9. None
  10. None
  11. None
  12. None
  13. 2ͭͷϧʔϧʹ΋ͱ͍ͮͯ શ෦ࣗಈԽ͠·ͨ͠

  14. 1. ΞΫγϣϯτϦΨʔ͕ ػցతʹݕ஌ՄೳͰ͋Δ͔

  15. None
  16. 1. ΞΫγϣϯτϦΨʔ͕ ػցతʹݕ஌Մೳ Ͱ͋Δ͔ ʹͰ͖Δ͔ {

  17. None
  18. None
  19. 2. ࣗಈԽͰ׬݁͢ΔͨΊͷ ౔୆͕Ͱ͖͍ͯΔ͔

  20. None
  21. None
  22. ϝʔϧˠ୺຤ొ࿥ͱ഑৴ ʹ৚݅Ϛον͢Δϝʔϧ͕དྷͨΒ Ͱߋ৽ɾ഑৴

  23. desc "[CI] add new device UDID. It requires options: name,

    udid" lane :add_new_device do |options| if (options[:name].nil? or options[:name].empty?) or (options[:udid].nil? or options[:udid].empty?) UI.user_error!("missing options! call with name:{device name} udid:{device udid}") end UI.important("registering #{options[:name]} (#{options[:udid]})") register_device( name: options[:name], udid: options[:udid] ) # ূ໌ॻߋ৽ match( app_identifier: ['com.myapp.viewer'], # ొ࿥͍ͨ͠ identifier ͢΂ͯೖΕΔ type: 'adhoc', # ඞཁͳΒ development ΋ force_for_new_devices: true ) # Ϗϧυ gym(...) # ഑৴ firebase_app_distribution(...) end fastlane add_new_device udid:xxx name:yyy
  24. import json # Zapier Ͱઃఆͨ͠؀ڥม਺͸ input_data ͰऔಘՄೳ headers = {

    'Accept': 'application/json', 'Content-Type': 'application/json' } auth = (input_data['circleci_token'], '') response = requests.post( "https://circleci.com/api/v2/project/gh/org/repo/pipeline", data=json.dumps({ "branch": "master", "parameters": { "run_device_registration_from_ci": True, "device_name": input_data['device_name'], "device_udid": input_data['device_udid'] }}), headers = headers, auth = auth) response.raise_for_status() output = response.json() parameters: run_device_registration_from_ci: type: boolean default: false workflows: version: 2.1 # --- API ܦ༝ͷΈ # fastlane register_device Λ࣮ߦ͢Δ add_new_device_from_ci: when: << pipeline.parameters.run_device_registration_from_ci >> steps: - checkout - run: *build - run: bundle exec fastlane add_new_device name:<< pipeline.parameters.device_name >> udid:<< pipeline.parameters.device_udid >> config.yml CI ্Ͱ fastlane ࣮ߦ
  25. None
  26. 1. ΞΫγϣϯτϦΨʔΛػցతʹݕ஌ՄೳͰ͋Δ͔ 2. ࣗಈԽͰ׬݁͢ΔͨΊͷ౔୆͕Ͱ͖͍ͯΔ͔

  27. ೖྗτϦΨʔ remote macOS server

  28. None
  29. ϦΞΫγϣϯˠissue΁ ͷൃݴʹ ͚ͭͨΒ issue ࡞Δ͔୳͢

  30. sample-channel

  31. ϏϧυΛ΋ͬͱ଎͍ͨ͘͠ ·ͣ͸ܭଌ͢Δ - צॴA - צॴB ϏϧυΛ΋ͬͱ଎͍ͨ͘͠ ·ͣ͸ܭଌ͢Δ - צॴA

    - צॴB ϏϧυΛ΋ͬͱ଎͍ͨ͘͠
  32. None
  33. None
  34. Slack ͷ౤ߘϦϯΫͱ ౤ߘऀ ຊจશ෦ description ʹ͍Ε͓ͯ͘

  35. 3. Ͱݟ͔ͭΒͳ͔ͬͨ = ৽ن࡞੒ͨ͠৔߹

  36. 3. Ͱݟ͔ͭͬͨ৔߹

  37. ϦΞΫγϣϯˠissue΁

  38. None
  39. None
  40. /command→CircleCI ͷ /command Webhook ΛΩϟονͨ͠Β Λ࣮ߦ͢Δ Ҿ਺ͷspace࡟আ Ҿ਺ check CircleCI

    API ௨஌
  41. /command→CircleCI Ҿ਺ͷspace࡟আ Ҿ਺ check CircleCI API ௨஌

  42. None
  43. /command→CircleCI ver. x.y.z Λ੾Γग़͢

  44. /command→CircleCI 3.1.0

  45. release branch ࡞੒ import json import base64 headers = {

    "Content-type": "application/json", "Authorization": f"token {input_data['github_token']}" } # master branch HEAD ͷ SHA Λऔಘ master_ref_url = "https://api.github.com/repos/org/repo/git/ref/heads/master" master_ref_response = requests.get(master_ref_url, headers=headers) master_ref_response.raise_for_status() master_ref_json = master_ref_response.json() master_sha = master_ref_json['object']['sha'] # release/x.y.z branch Λ࡞੒ release_branch_url = "https://api.github.com/repos/org/repo/git/refs" payload = { "ref": f"refs/heads/{input_data['branch']}", # branch = 'x.y.z' "sha": master_sha } release_branch_response = requests.post( release_branch_url, data=json.dumps(payload), headers=headers ) release_branch_response.raise_for_status() output = release_branch_response.json()
  46. CircleCI ্Ͱ࣮ߦ workflows: version: 2.1 auto_update_commit: when: << pipeline.parameters.run_auto_update_commit >>

    jobs: - "Commit and Push version update": filters: branches: only: /^release\/.+$ jobs: "Commit and Push version update": steps: - run: *checkout_and_build - run: name: "commit & push version update to ${NEXT_VERSION}" command: | NEXT_VERSION=<< pipeline.parameters.next_version >> git reset --hard HEAD git config user.email "ios-auto-commit@sample.com" git config user.name "your auto committer" bundle exec fastlane update_version to:${NEXT_VERSION} git add . git commit -m "[ci skip] [auto commit] update to ${NEXT_VERSION}" git push origin release/${NEXT_VERSION} - when: condition: << pipeline.parameters.run_integration_tests >> steps: *upload_latest_note_to_magic_pod release branch ͷΈ࣮ߦ fastlane update_version Λ࣮ߦͯ͠ commit & push
  47. fastlane update_version

  48. fastlane update_version desc "[CI] update version & deploy adhoc apps"

    lane :update_version do |options| increment_version_number(version_number: options[:to].to_s) generate_changelog match(...) gym(...) upload_to_firebase(...) end github-changelog-generator Ͱ 3.0.0 tag ~ HEAD ؒͷ diff Λऔͬͯ ReleaseNote Λ࡞੒
  49. /command→CircleCI

  50. None
  51. ϝʔϧˠSlack΁ ʹϚον͢Δϝʔϧ͕དྷͨΒ ੔ܗޙ emoji ͱจݴ੔ܗ ΁ྲྀ͢

  52. ϦϦʔεϝʔϧˠmerge

  53. release tag ࡞੒ # generator Ͱ࡞੒ͨ͠ ReleaseNote ͷຊจΛऔಘ branch =

    f"release/{input_data['version']}" release_note_url = f"https://api.github.com/repos/org/repo/contents/LatestReleaseCheckIssue.md?ref={branch}" body_response = requests.get(release_note_url, headers=headers) body_response.raise_for_status() body_json = body_response.json() decoded_body = base64.b64decode(body_json['content']).decode('utf-8') # version tag ࡞੒ release_tag_url = "https://api.github.com/repos/org/repo/releases" payload = { 'tag_name': input_data['version'], 'target_commitish': branch, 'name': input_data['version'], 'body': decoded_body, 'draft': False, 'prereleases': False } released_response = requests.post(release_tag_url, data=json.dumps(payload), headers=headers) released_response.raise_for_status() released_json = released_response.json()
  54. merge # master ʹ޲͚ͯ PR ࡞੒ pr_url = "https://api.github.com/repos/org/repo/pulls" payload

    = { 'title': f"[Auto Create & Merge] {input_data['version']}", 'head': branch, 'base': 'master', 'body': 'created by zapier' } pr_response = requests.post(pr_url, data=json.dumps(payload), headers=headers) pr_response.raise_for_status() pr_json = pr_response.json() pr_number = str(pr_json['number']) pr_link = f"https://github.com/org/repo/pull/{pr_number}" # merge merge_url = f"https://api.github.com/repos/org/repo/pulls/{pr_number}/merge" merge_response = requests.put(merge_url, headers=headers) result = merge_response.json() # Slack ʹ౤ߘ͢ΔϝοηʔδΛ࡞੒ʢauto merge ੒ޭ or ࣦഊʣ if 'merged' in result and result['merged'] == True: return { 'message': f"{input_data['version']} ϦϦʔεʂmaster merge ͨ͠Α {pr_link}" } else: return { 'message': f"{input_data['version']} ϦϦʔεʂ͚ͩͲ master merge Ͱ͖ͳ͔ͬͨΑ {pr_link}" }
  55. ϦϦʔεϝʔϧˠmerge

  56. ϦϦʔεϝʔϧˠdSYM

  57. ϦϦʔεϝʔϧˠdSYM lane :upload_dsym do |options| version = options[:version] || get_info_plist_value(path:

    "path/to/info.plist", key: "CFBundleShortVersionString") app_identifier = options[:app_identifier] || 'com.you.app' # ֘౰ ver. ͷ dSYM Λμ΢ϯϩʔυ sh 'rm -rf ../output/dsyms' sh 'mkdir -p ../output/dsyms' download_dsyms( version: version, app_identifier: app_identifier, output_directory: 'output/dsyms' ) Dir.chdir("../") do # FirebaseCrashlytics/upload-symbols ͰΞοϓϩʔυ͢ΔεΫϦϓτΛ࣮ߦ sh 'realpath output/dsyms/*.zip ɹɹɹɹɹ | xargs -I{} ./scripts/upload_dsyms.sh {} ./note/GoogleService-Info-Prod.plist' sh 'rm -rf output/dsyms' end end
  58. ϦϦʔεϝʔϧˠdSYM

  59. ʹ͗΍͔

  60. None
  61. To be continued...

  62. ٕज़ॻయͰ ຊग़͠·ͨ͠

  63. Thank you