# aesthetic computer (iOS) — App Store submission driven by an App Store # Connect API key. Same shape as apple/oskiewar and slab/menuband, for the # same reasons: one lane per irreversible step, screenshots owned by exactly # one lane, and nothing submits for review until `ship` is asked for by name. # # fastlane ios meta / shots / build / upload / privacy / ship # # The record already exists (id 6450940883, live as 1.0 since 2023-07-04), so # there is no `create` lane. The version and build number that land in App # Store Connect come from the Xcode project (MARKETING_VERSION and # CURRENT_PROJECT_VERSION on both targets — the iMessage extension has to # match the app), not from this file. VERSION and BUILD_NUMBER below only say # which App Store version the metadata and ship lanes address, so keep them # in step with the project when you bump it. require "shellwords" # the signing key's path reaches xcodebuild as an argument API_KEY_ID = "S4TQKG6U99" # Admin role — required for cloud signing. API_ISSUER = "69a6de78-fa3c-47e3-e053-5b8c7c11a4d1" API_KEY_PATH = File.expand_path("~/.appstoreconnect/private_keys/AuthKey_S4TQKG6U99.p8") BUNDLE = "aesthetic.computer" TEAM = "FB5948YR3S" VERSION = ENV.fetch("AC_IOS_VERSION", "1.2") BUILD_NUMBER = ENV.fetch("AC_IOS_BUILD_NUMBER", "5") # Everything is anchored to this Fastfile's own location so the lanes resolve # no matter where fastlane is invoked from. APPLE_DIR = File.expand_path("..", __dir__) PROJECT = File.join(APPLE_DIR, "aesthetic.computer.xcodeproj") SCHEME = "aesthetic.computer" # One path, written by `build` and read by `upload`. They must never disagree # — an `upload` lane pointed at a directory nobody exports to is a lane that # fails the first time anyone runs it. # Override for a one-off export: AC_IOS_IPA=build/export-1.2/aesthetic.computer.ipa IPA = File.expand_path(ENV.fetch("AC_IOS_IPA", "build/export/aesthetic.computer.ipa"), APPLE_DIR) # The last mile is Menu Band's. asc.mjs attaches the newest VALID build and # opens the review submission idempotently, and refuses while a build is still # processing. deliver's own submit path died with "No data" for Menu Band, so # it is not trusted with the submission here either. ASC = File.expand_path("../slab/menuband/bin/asc.mjs", APPLE_DIR) PRIVACY_JSON = File.join(__dir__, "app_privacy_details.json") # Without these three, -allowProvisioningUpdates has no credentials to mint a # certificate with and falls back to whatever is already in the keychain, # which on a fresh machine is nothing. AUTH_XCARGS = "-allowProvisioningUpdates " \ "-authenticationKeyPath #{API_KEY_PATH.shellescape} " \ "-authenticationKeyID #{API_KEY_ID} " \ "-authenticationKeyIssuerID #{API_ISSUER}" def asc_key app_store_connect_api_key( key_id: API_KEY_ID, issuer_id: API_ISSUER, key_filepath: API_KEY_PATH, in_house: false, ) end # Every deliver call in this file agrees on these: never guess at a review # submission, never let precheck's guideline scan gate an upload, and always # overwrite rather than fail on an existing draft. def deliver_defaults { api_key: asc_key, platform: "ios", app_identifier: BUNDLE, submit_for_review: false, force: true, run_precheck_before_submit: false, # deliver validates these directories on EVERY call, even with the # matching skip_* flag set, and rejects the platform subfolder as an # unknown locale — so the platform-specific paths live here, not per lane. metadata_path: "./fastlane/metadata/ios", screenshots_path: "./fastlane/screenshots/ios", } end platform :ios do # 1. Metadata only. Creates the App Store version when it does not exist # yet. Safe to re-run. lane :meta do deliver(**deliver_defaults, app_version: VERSION, skip_screenshots: true, skip_binary_upload: true) end # 1b. Screenshots have one owner. Two deliver actions racing the same # version leave byte-identical duplicates in App Store Connect. lane :shots do deliver(**deliver_defaults, app_version: VERSION, skip_metadata: true, skip_binary_upload: true, overwrite_screenshots: true) end # 2. Archive and export an App Store .ipa. Signing is automatic and # cloud-issued: -allowProvisioningUpdates plus an Admin API key lets # xcodebuild mint the Apple Distribution certificate and the App Store # profiles for both the app and the iMessage extension without anyone # opening the portal. The same credentials go to the export step, which # is where the re-signing actually happens. lane :build do # A device archive needs the iPhoneOS SDK, not an installed simulator. sdk = sh("xcrun --sdk iphoneos --show-sdk-path", log: false).strip UI.user_error!("Xcode has no iPhoneOS SDK installed") unless File.directory?(sdk) build_ios_app( project: PROJECT, scheme: SCHEME, configuration: "Release", export_method: "app-store", export_team_id: TEAM, xcargs: AUTH_XCARGS, export_xcargs: AUTH_XCARGS, output_directory: File.dirname(IPA), output_name: File.basename(IPA, ".ipa"), ) end # 3. Upload that .ipa and nothing else, which starts App Store processing. lane :upload do UI.user_error!("no ipa at #{IPA} — run `fastlane ios build` first") unless File.exist?(IPA) deliver(**deliver_defaults, ipa: IPA, skip_metadata: true, skip_screenshots: true) end # 3b. Publish the App Privacy nutrition label. The public API has no # appDataUsages endpoint, so this drives Apple's private routes with the # same key. Only once the JSON exists and agrees with what the binary # really does. lane :privacy do UI.user_error!("author #{PRIVACY_JSON} first — apple/oskiewar/fastlane/app_privacy_details.json is the shape") unless File.exist?(PRIVACY_JSON) upload_app_privacy_details( api_key: asc_key, app_identifier: BUNDLE, json_path: PRIVACY_JSON, ) end # 4. Submit for review: attach the newest processed build to VERSION and # open the review submission. Irreversible, so it only runs by name. lane :ship do sh("node", ASC, "submit", VERSION, "--app", "aestheticcomputer", "--platform", "IOS") end end