diff --git a/.ci/bundler_version.rb b/.ci/bundler_version.rb new file mode 100755 index 00000000000..9ed0a9f2e08 --- /dev/null +++ b/.ci/bundler_version.rb @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +file = File.join(__dir__, "..", "Gemfile.lock") +lines = File.read(file).split("\n") +idx = lines.index { |l| l == "BUNDLED WITH" } +puts lines[idx + 1].strip diff --git a/.ci/compatible_gem_version b/.ci/compatible_gem_version new file mode 100755 index 00000000000..7e19cef0ac5 --- /dev/null +++ b/.ci/compatible_gem_version @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# +# prints out the latest version number of the gem compatible with the given version of ruby +# +require 'net/http' +require 'json' + +GEM_TO_TEST = "rubygems-update" + +if ARGV.count > 0 + RUBY_VERSION_TO_MATCH = ARGV[0] +else + RUBY_VERSION_TO_MATCH = RUBY_VERSION +end + +API_URL = "https://rubygems.org/api/v1/versions/#{GEM_TO_TEST}.json" + +# Load list of all available versions of GEM_TO_TEST +gem_versions = JSON.parse(Net::HTTP.get(URI(API_URL))) + +# Process list to find matching Ruby version +matching_gem = gem_versions.find { |gem| + Gem::Dependency.new('', gem['ruby_version']). + match?('', RUBY_VERSION_TO_MATCH) +} + +puts matching_gem['number'] diff --git a/.circleci/config.yml b/.circleci/config.yml index 12c05377e61..37866d85201 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -55,9 +55,11 @@ aliases: run: name: bundle install command: | + gem update --system `.ci/compatible_gem_version` gem install bundler -v $(cat Gemfile.lock | tail -1 | tr -d " ") bundle config set --local path .bundle bundle check || bundle install --jobs=4 --retry=3 + bundle env jobs: tests_macos: &tests_macos_base @@ -101,6 +103,7 @@ jobs: - run: name: bundle exec fastlane execute_tests command: | + ulimit -n 65536 # allows us to stress test the system if we want bundle exec fastlane snapshot reset_simulators --force RUBYOPT=<< parameters.ruby_opt >> bundle exec fastlane execute_tests - *cache_save_rubocop @@ -177,19 +180,6 @@ jobs: version: << parameters.ruby_version >> - *bundle_install - *cache_save_bundler - - run: - name: debug | brew version - command: | - brew -v - - run: - name: brew update if needed # temporary solution for this https://discuss.circleci.com/t/macos-image-users-homebrew-brownout-2021-04-26/39872/2 - command: | - # if the version is not 3.x.x, try brew update - ruby -e 'exit(1) if `brew -v` =~ /2\.\d+\.\d+/' ||\ - brew update || \ - # if brew update fails due to shallow clone, try unshallowing and then brew update again - git -C /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core fetch --unshallow && \ - brew update - run: bundle exec fastlane generate_swift_api validate_documentation: @@ -209,7 +199,7 @@ jobs: - run: name: Setup Build command: | - gem update --system + gem update --system `.ci/compatible_gem_version` - *bundle_install - *cache_save_bundler @@ -260,27 +250,10 @@ workflows: version: 2 "Run Tests & Checks": # Name of the workflow, which ends up displayed on GitHub's PR Check jobs: - - tests_macos: - name: 'Execute tests on macOS (Xcode 12.5.1, Ruby 2.6)' - xcode_version: '12.5.1' - ruby_version: '2.6.6' - <<: *important-branches - - tests_macos: - name: 'Execute tests on macOS (Xcode 12.5.1, Ruby 2.7)' - xcode_version: '12.5.1' - ruby_version: '2.7.3' - ruby_opt: -W:deprecated - <<: *important-branches - - tests_macos: - name: 'Execute tests on macOS (Xcode 13.4.1, Ruby 3.0)' - xcode_version: '13.4.1' - ruby_version: '3.0.4' - ruby_opt: -W:deprecated - <<: *important-branches - tests_macos: name: 'Execute tests on macOS (Xcode 13.4.1, Ruby 3.1)' xcode_version: '13.4.1' - ruby_version: '3.1.2' + ruby_version: '3.1' ruby_opt: -W:deprecated - tests_macos: name: 'Execute tests on macOS (Xcode 14.3.1, Ruby 3.1)' @@ -292,11 +265,6 @@ workflows: xcode_version: '15.0.1' ruby_version: '3.1' ruby_opt: -W:deprecated - - tests_macos: - name: 'Execute tests on macOS (Xcode 13.4.1, Ruby 3.1)' - xcode_version: '13.4.1' - ruby_version: '3.1' - ruby_opt: -W:deprecated - tests_macos: name: 'Execute tests on macOS (Xcode 14.3.1, Ruby 3.2)' xcode_version: '14.3.1' @@ -307,14 +275,23 @@ workflows: xcode_version: '15.0.1' ruby_version: '3.2' ruby_opt: -W:deprecated + - tests_macos: + name: 'Execute tests on macOS (Xcode 15.2.0, Ruby 3.2)' + xcode_version: '15.2.0' + ruby_version: '3.2' + - tests_macos: + name: 'Execute tests on macOS (Xcode 15.3, Ruby 3.3)' + xcode_version: '15.3' + ruby_version: '3.3' + ruby_opt: -W:deprecated - tests_ubuntu: name: 'Execute tests on Ubuntu' image: 'fastlanetools/ci:0.3.0' ruby_version: '2.6' - validate_fastlane_swift_generation: name: 'Validate Fastlane.swift generation' - xcode_version: '12.5.1' - ruby_version: '2.7' + xcode_version: '15.0.1' + ruby_version: '3.2' - validate_documentation: name: 'Validate Documentation' image: 'cimg/ruby:3.2.2' diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000000..5a01aeae463 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,103 @@ +# Configuration file for https://github.com/actions/labeler + +"tool: action": + - changed-files: + - any-glob-to-any-file: + - fastlane/lib/fastlane/actions/**/* + +"tool: cert": + - changed-files: + - any-glob-to-any-file: + - cert/**/* + +"tool: deliver": + - changed-files: + - any-glob-to-any-file: + - deliver/**/* + +"tool: fastlane_core": + - changed-files: + - any-glob-to-any-file: + - fastlane_core/**/* + +"tool: frameit": + - changed-files: + - any-glob-to-any-file: + - frameit/**/* + +"tool: gym": + - changed-files: + - any-glob-to-any-file: + - gym/**/* + +"tool: match": + - changed-files: + - any-glob-to-any-file: + - match/**/* + +"tool: pem": + - changed-files: + - any-glob-to-any-file: + - pem/**/* + +"tool: pilot": + - changed-files: + - any-glob-to-any-file: + - pilot/**/* + +"tool: precheck": + - changed-files: + - any-glob-to-any-file: + - precheck/**/* + +"tool: produce": + - changed-files: + - any-glob-to-any-file: + - produce/**/* + +"tool: scan": + - changed-files: + - any-glob-to-any-file: + - scan/**/* + +"tool: screengrab": + - changed-files: + - any-glob-to-any-file: + - screengrab/**/* + +"tool: sigh": + - changed-files: + - any-glob-to-any-file: + - sigh/**/* + +"tool: snapshot": + - changed-files: + - any-glob-to-any-file: + - snapshot/**/* + +"tool: spaceship": + - changed-files: + - any-glob-to-any-file: + - spaceship/**/* + +"tool: supply": + - changed-files: + - any-glob-to-any-file: + - supply/**/* + +"topic: swift": + - changed-files: + - any-glob-to-any-file: + - fastlane/swift/**/* + +"tool: trainer": + - changed-files: + - any-glob-to-any-file: + - trainer/**/* + +"type: ci": + - changed-files: + - any-glob-to-any-file: + - .github/**/* + - .circleci/**/* + - appveyor.yml diff --git a/.github/workflows/release.yml b/.github/workflows/announce_release.yml similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows/announce_release.yml diff --git a/.github/workflows/create_tag_on_bump_merge.yml b/.github/workflows/create_tag_on_bump_merge.yml new file mode 100644 index 00000000000..0dc0d6b20da --- /dev/null +++ b/.github/workflows/create_tag_on_bump_merge.yml @@ -0,0 +1,28 @@ +name: Create Tag Version on Bump Merge + +on: + pull_request: + types: [closed] + +jobs: + version_bump: + if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'version-bump-2.221.0-') + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Read gem version + id: read_version + run: | + gemspec_file=$(ls *.gemspec) + version=$(grep -E "spec.version\s*=\s*['\"][0-9]+\.[0-9]+\.[0-9]+['\"]" $gemspec_file | grep -o -E "[0-9]+\.[0-9]+\.[0-9]+") + echo "version=$version" >> $GITHUB_ENV + + - name: Create Tag + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git tag -a v${{ env.version }} -m "Version ${{ env.version }}" + git push origin --tags \ No newline at end of file diff --git a/.github/workflows/deploy_to_rubygems.yml b/.github/workflows/deploy_to_rubygems.yml new file mode 100644 index 00000000000..d68280c8f69 --- /dev/null +++ b/.github/workflows/deploy_to_rubygems.yml @@ -0,0 +1,23 @@ +--- +name: Deploy to RubyGems +on: + release: + types: + - published + workflow_dispatch: + +jobs: + deploy-to-rubygems: + name: Deploy to RubyGems + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: '3.2' + - uses: rubygems/release-gem@v1 \ No newline at end of file diff --git a/.github/workflows/pull_request_labeler.yml b/.github/workflows/pull_request_labeler.yml new file mode 100644 index 00000000000..74355012bcc --- /dev/null +++ b/.github/workflows/pull_request_labeler.yml @@ -0,0 +1,16 @@ +name: "Pull Request Labeler" +on: + pull_request_target: + types: + - opened + - synchronize # Add labels when PR receives new commits, which may modify the files in the PR + - reopened + +jobs: + labeler: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v5 diff --git a/.github/workflows/release_step_1_create_version_bump.yml b/.github/workflows/release_step_1_create_version_bump.yml new file mode 100644 index 00000000000..9c846acf289 --- /dev/null +++ b/.github/workflows/release_step_1_create_version_bump.yml @@ -0,0 +1,49 @@ +--- +name: Release Step 1 - Create Version Bump + +on: + workflow_dispatch: + inputs: + bump_type: + description: 'Bump type' + required: true + type: choice + options: + - patch + - minor + - major + +permissions: + contents: write + pull-requests: write + +jobs: + create_version_bump: + runs-on: macos-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2 + + - name: Install fastlane + run: bundle install + + - name: Run fastlane bump + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + bundle exec fastlane bump bump_type:${{ github.event.inputs.bump_type }} + env: + GITHUB_USER_NAME: fastlane # Todo: This is needed for docs - remove somehow + GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Todo: This is needed for docs - remove somehow + GITHUB_API_BEARER: ${{ secrets.GITHUB_TOKEN }} + FL_GITHUB_RELEASE_API_BEARER: ${{ secrets.GITHUB_TOKEN }} + FL_CHANGELOG_SLEEP: 10 + FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 180 + FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 5 + diff --git a/.github/workflows/release_step_2_create_github_release.yml b/.github/workflows/release_step_2_create_github_release.yml new file mode 100644 index 00000000000..8c82ae2ed2a --- /dev/null +++ b/.github/workflows/release_step_2_create_github_release.yml @@ -0,0 +1,55 @@ +--- +name: Release Step 2 - Create GitHub Release + +on: + push: + tags: + - '*' + workflow_dispatch: + inputs: + skip_github_packages: + description: 'Skip pushing to GitHub Packages' + required: true + default: false + type: boolean + skip_rubygems: + description: 'Skip pushing to RubyGems' + required: true + default: false + type: boolean + +jobs: + create_github_release: + runs-on: macos-latest + + permissions: + contents: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + + - name: Install fastlane + run: bundle install + + - uses: rubygems/configure-rubygems-credentials@v1.0.0 + + - name: Configure GitHub Packages config + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mkdir -p ~/.gem + echo ":github: Bearer $GITHUB_TOKEN" > ~/.gem/credentials + chmod 0600 ~/.gem/credentials + + - name: Run fastlane + env: + FASTLANE_RELEASE_API_BEARER: ${{ secrets.GITHUB_TOKEN }} + run: | + bundle exec fastlane create_github_release skip_github_packages:${{ github.event.inputs.skip_github_packages }} skip_rubygems:${{ github.event.inputs.skip_rubygems }} \ No newline at end of file diff --git a/CHANGELOG.latest.md b/CHANGELOG.latest.md new file mode 100644 index 00000000000..414027caf7e --- /dev/null +++ b/CHANGELOG.latest.md @@ -0,0 +1,3 @@ +* [core] fix duplicate builds being matched in BuildWatcher (#22256) via Josh Holtz (@joshdholtz) +* [ci] adding one more github auth call to CI release process (#22253) via Josh Holtz (@joshdholtz) +* [c] prevent rate limited when releasing fastlane on GitHub Actions (#22252) via Josh Holtz (@joshdholtz) \ No newline at end of file diff --git a/Gemfile b/Gemfile index 0c80082de52..9fabd103045 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,8 @@ gem "danger-junit", "~> 1.0" # A fake filesystem. # Version 1.9+ requires Ruby >=2.7, while fastlane uses a `required_ruby_version` of `>= 2.6`. gem "fakefs", "1.8" +# for file uploads with Faraday +gem "mime-types", ['>= 1.16', '< 4.0'] # Fast XML parser and object marshaller. gem "ox", "2.13.2" # Provides an interactive debugging environment for Ruby. @@ -31,8 +33,6 @@ gem "rake" # A readline implementation in Ruby # See: https://github.com/deivid-rodriguez/byebug/issues/289#issuecomment-251383465 gem "rb-readline" -# A simple and flexible REST client. -gem "rest-client", ">= 1.8.0" # Behavior-driven testing tool for Ruby. gem "rspec", "~> 3.10" # Formatter for RSpec to generate JUnit compatible reports. diff --git a/Gemfile.lock b/Gemfile.lock index 085a3b9b3ab..44339bcf217 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,14 +1,14 @@ PATH remote: . specs: - fastlane (2.219.0) + fastlane (2.223.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -29,10 +29,10 @@ PATH mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (>= 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -41,7 +41,7 @@ PATH word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) GEM remote: https://rubygems.org/ @@ -54,7 +54,7 @@ GEM ast (2.4.2) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.876.0) + aws-partitions (1.877.0) aws-sdk-core (3.190.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) @@ -116,11 +116,10 @@ GEM digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) docile (1.3.5) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.108.0) + excon (0.109.0) fakefs (1.8.0) faraday (1.4.1) faraday-excon (~> 1.1) @@ -185,7 +184,6 @@ GEM signet (>= 0.16, < 2.a) hashdiff (1.0.1) highline (2.0.3) - http-accept (1.7.0) http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) @@ -198,9 +196,9 @@ GEM kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) method_source (1.0.0) - mime-types (3.3.1) + mime-types (3.5.2) mime-types-data (~> 3.2015) - mime-types-data (3.2021.0225) + mime-types-data (3.2024.0206) mini_magick (4.12.0) mini_mime (1.1.5) multi_json (1.15.0) @@ -210,7 +208,6 @@ GEM nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) - netrc (0.11.0) no_proxy_fix (0.1.2) octokit (4.25.1) faraday (>= 1, < 3) @@ -250,11 +247,6 @@ GEM declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - rest-client (2.1.0) - http-accept (>= 1.7.0, < 2.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 4.0) - netrc (~> 0.8) retriable (3.1.2) rexml (3.2.6) rouge (2.0.7) @@ -296,7 +288,7 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - security (0.1.3) + security (0.1.5) signet (0.18.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -332,9 +324,6 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) unicode-display_width (2.4.2) webmock (3.18.1) addressable (>= 2.8.0) @@ -377,6 +366,7 @@ DEPENDENCIES fastlane-plugin-clubmate fastlane-plugin-ruby fastlane-plugin-slack_train (>= 0.2.0) + mime-types (>= 1.16, < 4.0) ox (= 2.13.2) pry pry-byebug @@ -384,7 +374,6 @@ DEPENDENCIES pry-stack_explorer rake rb-readline - rest-client (>= 1.8.0) rspec (~> 3.10) rspec_junit_formatter (~> 0.4.1) rubocop (= 1.50.2) diff --git a/README.md b/README.md index c7040de0702..d5caa86759b 100644 --- a/README.md +++ b/README.md @@ -35,81 +35,43 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/ - - - - - - - - - - - - - - + + - - - - @@ -169,23 +137,37 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/

Luka Mirosevic

- - - + + + + + + + -
- - - -

Manu Wallner

-
- - - -

Joshua Liebowitz

-

Danielle Tomlinson

- - - -

Satoshi Namai

-
- - - -

Felix Krause

-
- - - -

Łukasz Grabowski

-
- - + + + -

Maksym Grebenets

+

Jan Piotrowski

- - + + + -

Fumiya Nakamura

+

Helmut Januschka

- - + + + -

Jérôme Lacoste

+

Aaron Brager

- - + + + -

Stefan Natchev

+

Iulian Onofrei

- - - -

Helmut Januschka

-
- - + + + -

Manish Rathi

+

Matthew Ellis

@@ -117,25 +79,31 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/

Max Ott

- - - -

Jimmy Dee

-

Roger Oba

+ + + +

Olivier Halligon

+
+ + + +

Manish Rathi

+
- - + + + -

Iulian Onofrei

+

Felix Krause

@@ -143,23 +111,23 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/

Josh Holtz

- - + + + -

Daniel Jankowski

+

Łukasz Grabowski

- - + + + -

Andrew McBurney

+

Fumiya Nakamura

- - + + + -

Jan Piotrowski

+

Maksym Grebenets

- - + + + -

Kohki Miki

+

Jérôme Lacoste

- - + + + -

Aaron Brager

+

Andrew McBurney

- - + + + -

Olivier Halligon

+

Stefan Natchev

+
+ + + +

Manu Wallner

+
+ + + +

Daniel Jankowski

@@ -193,13 +175,31 @@ If the above doesn't help, please [submit an issue](https://github.com/fastlane/

Jorge Revuelta H

+ + + +

Joshua Liebowitz

+
+ + + +

Jimmy Dee

+
+ + + +

Satoshi Namai

+
- - + + + -

Matthew Ellis

+

Kohki Miki

diff --git a/ToolsAndDebugging.md b/ToolsAndDebugging.md index 775eb80057f..c58066576c4 100644 --- a/ToolsAndDebugging.md +++ b/ToolsAndDebugging.md @@ -2,6 +2,12 @@ For detailed instructions on how to get started with contributing to _fastlane_, first check out [YourFirstPR.md][first-pr] and [Testing.md](Testing.md). This guide will focus on more advanced instructions on how to debug _fastlane_ and _spaceship_ issues and work on patches. +## Experiment with the _fastlane_ internals + +Open a _fastlane_ console by running `fastlane console` inside or outside your project. + +This will allow you to invoke any of the _fastlane_ modules and classes and test their behavior. + ## Debug using [pry](https://pry.github.io/) Before you’re able to use [pry](https://pry.github.io/), make sure to have completed the [YourFirstPR.md][first-pr] setup part, as this will install all required development dependencies. @@ -28,6 +34,10 @@ DEBUG=1 bundle exec rspec You will then jump into an interactive debugger that allows you to print out variables, call methods and [much more](https://github.com/pry/pry/wiki). To continue running the original script use `control` + `d` +## Running fastlane within a IRB console + +You can open an IRB console by running `bin/console` + ## Debugging and patching [_spaceship_](https://github.com/fastlane/fastlane/tree/master/spaceship) problems ### Introduction to _spaceship_ @@ -94,6 +104,10 @@ To run the newly created script, run SPACESHIP_DEBUG=1 bundle exec rake debug ``` +### Running the spaceship playground + +You can open an interactive _spaceship_ session console by running `fastlane spaceship` + ### Additional Information See also the [Debugging _spaceship_](spaceship/docs/Debugging.md) documentation. diff --git a/appveyor.yml b/appveyor.yml index dc72a1e08d8..fe37ad94cb5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,6 +24,8 @@ init: install: - set PATH=C:\Ruby26-x64\bin;%PATH% + - FOR /F "usebackq delims==" %%A IN (`ruby .ci/compatible_gem_version`) DO gem update --system %%A + - FOR /F "usebackq delims==" %%A IN (`ruby .ci/bundler_version.rb`) DO gem install bundler -v %%A - bundle install # iconv - appveyor DownloadFile diff --git a/bin/console b/bin/console new file mode 100755 index 00000000000..2fb641ed79b --- /dev/null +++ b/bin/console @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "fastlane" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "irb" +IRB.start(__FILE__) diff --git a/bin/match_file b/bin/match_file new file mode 100755 index 00000000000..7ee7232c23f --- /dev/null +++ b/bin/match_file @@ -0,0 +1,60 @@ +#!/usr/bin/env ruby +require 'match' + +# CLI to encrypt/decrypt files using fastlane match encryption layer + +def usage + puts("USAGE: [encrypt|decrypt] input_path [output_path]") + exit(-1) +end + +if ARGV.count < 2 || ARGV.count > 3 + usage +end + +method_name = ARGV.shift +unless ['encrypt', 'decrypt'].include?(method_name) + usage +end + +input_file = ARGV.shift + +if ARGV.count > 0 + output_file = ARGV.shift +else + output_file = input_file +end + +def ask_password(msg) + ask(msg) do |q| + q.whitespace = :chomp + q.echo = "*" + end +end + +def ask_password_twice + password = ask_password("Enter the password: ") + return "" if password.empty? || password == "\u0003" # CTRL-C char + other = ask_password("Enter the password again: ") + if other == password + return password + else + return nil + end +end + +# read the password +password = nil +loop do + password = ask_password_twice + break unless password.nil? +end + +exit if password.empty? + +begin + Match::Encryption::MatchFileEncryption.new.send(method_name, file_path: input_file, password: password, output_path: output_file) +rescue => e + puts("ERROR #{method_name}ing. [#{e}]. Check your password") + usage +end diff --git a/deliver/lib/deliver/download_screenshots.rb b/deliver/lib/deliver/download_screenshots.rb index 70e6b919077..da198d3784b 100644 --- a/deliver/lib/deliver/download_screenshots.rb +++ b/deliver/lib/deliver/download_screenshots.rb @@ -1,5 +1,6 @@ require_relative 'module' require 'spaceship' +require 'open-uri' module Deliver class DownloadScreenshots @@ -67,7 +68,7 @@ def self.download_screenshots(folder_path, localization) end path = File.join(containing_folder, file_name) - File.binwrite(path, FastlaneCore::Helper.open_uri(url).read) + File.binwrite(path, URI.open(url).read) end end end diff --git a/deliver/lib/deliver/generate_summary.rb b/deliver/lib/deliver/generate_summary.rb index 5088f0b456e..a1a7380dd53 100644 --- a/deliver/lib/deliver/generate_summary.rb +++ b/deliver/lib/deliver/generate_summary.rb @@ -6,7 +6,7 @@ module Deliver class GenerateSummary def run(options) screenshots = UploadScreenshots.new.collect_screenshots(options) - UploadMetadata.new.load_from_filesystem(options) + UploadMetadata.new(options).load_from_filesystem HtmlGenerator.new.render(options, screenshots, '.') end end diff --git a/deliver/lib/deliver/options.rb b/deliver/lib/deliver/options.rb index 5ff0929d977..1659c24ecad 100644 --- a/deliver/lib/deliver/options.rb +++ b/deliver/lib/deliver/options.rb @@ -99,7 +99,7 @@ def self.available_options optional: true, default_value: "ios", verify_block: proc do |value| - UI.user_error!("The platform can only be ios, appletvos, or osx") unless %('ios', 'appletvos', 'osx').include?(value) + UI.user_error!("The platform can only be ios, appletvos, xros or osx") unless %('ios', 'appletvos', 'xros', 'osx').include?(value) end), # live version @@ -162,6 +162,11 @@ def self.available_options description: "Clear all previously uploaded screenshots before uploading the new ones", type: Boolean, default_value: false), + FastlaneCore::ConfigItem.new(key: :screenshot_processing_timeout, + env_name: "DELIVER_SCREENSHOT_PROCESSING_TIMEOUT", + description: "Timeout in seconds to wait before considering screenshot processing as failed, used to handle cases where uploads to the App Store are stuck in processing", + type: Integer, + default_value: 3600), FastlaneCore::ConfigItem.new(key: :sync_screenshots, env_name: "DELIVER_SYNC_SCREENSHOTS", description: "Sync screenshots with local ones. This is currently beta option so set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well", @@ -182,6 +187,15 @@ def self.available_options description: "Rejects the previously submitted build if it's in a state where it's possible", type: Boolean, default_value: false), + FastlaneCore::ConfigItem.new(key: :version_check_wait_retry_limit, + env_name: "DELIVER_VERSION_CHECK_WAIT_RETRY_LIMIT", + description: "After submitting a new version, App Store Connect takes some time to recognize the new version and we must wait until it's available before attempting to upload metadata for it. There is a mechanism that will check if it's available and retry with an exponential backoff if it's not available yet. " \ + "This option specifies how many times we should retry before giving up. Setting this to a value below 5 is not recommended and will likely cause failures. Increase this parameter when Apple servers seem to be degraded or slow", + type: Integer, + default_value: 7, + verify_block: proc do |value| + UI.user_error!("'#{value}' needs to be greater than 0") if value <= 0 + end), # release FastlaneCore::ConfigItem.new(key: :automatic_release, @@ -235,7 +249,7 @@ def self.available_options end), FastlaneCore::ConfigItem.new(key: :submission_information, short_option: "-b", - description: "Extra information for the submission (e.g. compliance specifications, IDFA settings)", + description: "Extra information for the submission (e.g. compliance specifications)", type: Hash, optional: true), diff --git a/deliver/lib/deliver/runner.rb b/deliver/lib/deliver/runner.rb index 6529aaffe94..1f8985160f5 100644 --- a/deliver/lib/deliver/runner.rb +++ b/deliver/lib/deliver/runner.rb @@ -135,21 +135,21 @@ def verify_version # Upload all metadata, screenshots, pricing information, etc. to App Store Connect def upload_metadata - upload_metadata = UploadMetadata.new + upload_metadata = UploadMetadata.new(options) upload_screenshots = UploadScreenshots.new # First, collect all the things for the HTML Report screenshots = upload_screenshots.collect_screenshots(options) - upload_metadata.load_from_filesystem(options) + upload_metadata.load_from_filesystem # Assign "default" values to all languages - upload_metadata.assign_defaults(options) + upload_metadata.assign_defaults # Validate validate_html(screenshots) # Commit - upload_metadata.upload(options) + upload_metadata.upload if options[:sync_screenshots] sync_screenshots = SyncScreenshots.new(app: Deliver.cache[:app], platform: Spaceship::ConnectAPI::Platform.map(options[:platform])) @@ -172,7 +172,7 @@ def verify_binary transporter = transporter_for_selected_team case platform - when "ios", "appletvos" + when "ios", "appletvos", "xros" package_path = FastlaneCore::IpaUploadPackageBuilder.new.generate( app_id: Deliver.cache[:app].id, ipa_path: ipa_path, @@ -209,7 +209,7 @@ def upload_binary transporter = transporter_for_selected_team case platform - when "ios", "appletvos" + when "ios", "appletvos", "xros" package_path = FastlaneCore::IpaUploadPackageBuilder.new.generate( app_id: Deliver.cache[:app].id, ipa_path: ipa_path, @@ -264,6 +264,7 @@ def submit_for_review private # If App Store Connect API token, use token. + # If api_key is specified and it is an Individual API Key, don't use token but use username. # If itc_provider was explicitly specified, use it. # If there are multiple teams, infer the provider from the selected team name. # If there are fewer than two teams, don't infer the provider. @@ -280,6 +281,14 @@ def transporter_for_selected_team api_key end + # Currently no kind of transporters accept an Individual API Key. Use username and app-specific password instead. + # See https://github.com/fastlane/fastlane/issues/22115 + is_individual_key = !api_key.nil? && api_key[:issuer_id].nil? + if is_individual_key + api_key = nil + api_token = nil + end + unless api_token.nil? api_token.refresh! if api_token.expired? return FastlaneCore::ItunesTransporter.new(nil, nil, false, nil, api_token.text, altool_compatible_command: true, api_key: api_key) diff --git a/deliver/lib/deliver/submit_for_review.rb b/deliver/lib/deliver/submit_for_review.rb index b4d36503b8c..caa325ceb6b 100644 --- a/deliver/lib/deliver/submit_for_review.rb +++ b/deliver/lib/deliver/submit_for_review.rb @@ -20,7 +20,6 @@ def submit!(options) build = select_build(options, app, version, platform) update_export_compliance(options, app, build) - update_idfa(options, app, version) update_submission_information(options, app) create_review_submission(options, app, version, platform) @@ -51,11 +50,11 @@ def create_review_submission(options, app, version, platform) 10.times do version_with_latest_info = Spaceship::ConnectAPI::AppStoreVersion.get(app_store_version_id: version.id) - if version_with_latest_info.app_store_state == Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::READY_FOR_REVIEW + if version_with_latest_info.app_version_state == Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::READY_FOR_REVIEW break end - UI.message("Waiting for the state of the version to become #{Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::READY_FOR_REVIEW}...") + UI.message("Waiting for the state of the version to become #{Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::READY_FOR_REVIEW}...") sleep(15) end @@ -120,82 +119,6 @@ def update_export_compliance(options, app, build) end end - def update_idfa(options, app, version) - submission_information = options[:submission_information] || {} - submission_information = submission_information.transform_keys(&:to_sym) - - uses_idfa = submission_information[:add_id_info_uses_idfa] - - idfa_declaration = begin - version.fetch_idfa_declaration - rescue - nil - end - - updated_idfa = false - - # Set IDFA on version - unless uses_idfa.nil? - UI.verbose("Updating app store version for IDFA status of '#{uses_idfa}'") - version = version.update(attributes: { - usesIdfa: uses_idfa - }) - UI.verbose("Updated app store version for IDFA status of '#{version.uses_idfa}'") - updated_idfa = true - end - - # Error if uses_idfa not set - if version.uses_idfa.nil? - message = [ - "Use of Advertising Identifier (IDFA) is required to submit", - "Add information to the :submission_information option...", - " Docs: http://docs.fastlane.tools/actions/deliver/#compliance-and-idfa-settings", - " Example: submission_information: { add_id_info_uses_idfa: false }", - " Example: submission_information: {", - " add_id_info_uses_idfa: true,", - " add_id_info_serves_ads: false,", - " add_id_info_tracks_install: true,", - " add_id_info_tracks_action: true,", - " add_id_info_limits_tracking: true", - " }", - " Example CLI:", - " --submission_information \"{\\\"add_id_info_uses_idfa\\\": false}\"" - ].join("\n") - UI.user_error!(message) - end - - # Create, update, or delete IDFA declaration - if uses_idfa == false - if idfa_declaration - UI.verbose("Deleting IDFA declaration") - idfa_declaration.delete! - updated_idfa = true - UI.verbose("Deleted IDFA declaration") - end - elsif uses_idfa == true - attributes = { - honorsLimitedAdTracking: !!submission_information[:add_id_info_limits_tracking], - servesAds: !!submission_information[:add_id_info_serves_ads], - attributesAppInstallationToPreviousAd: !!submission_information[:add_id_info_tracks_install], - attributesActionWithPreviousAd: !!submission_information[:add_id_info_tracks_action] - } - - if idfa_declaration - UI.verbose("Updating IDFA declaration") - idfa_declaration.update(attributes: attributes) - UI.verbose("Updated IDFA declaration") - else - UI.verbose("Creating IDFA declaration") - version.create_idfa_declaration(attributes: attributes) - UI.verbose("Created IDFA declaration") - end - - updated_idfa = true - end - - UI.success("Successfully updated IDFA declarations on App Store Connect") if updated_idfa - end - def update_submission_information(options, app) submission_information = options[:submission_information] || {} submission_information = submission_information.transform_keys(&:to_sym) diff --git a/deliver/lib/deliver/upload_metadata.rb b/deliver/lib/deliver/upload_metadata.rb index 2447f2e37ec..8e9f523bed6 100644 --- a/deliver/lib/deliver/upload_metadata.rb +++ b/deliver/lib/deliver/upload_metadata.rb @@ -78,19 +78,25 @@ class UploadMetadata require_relative 'loader' + attr_accessor :options + + def initialize(options) + self.options = options + end + # Make sure to call `load_from_filesystem` before calling upload - def upload(options) + def upload return if options[:skip_metadata] app = Deliver.cache[:app] platform = Spaceship::ConnectAPI::Platform.map(options[:platform]) - enabled_languages = detect_languages(options) + enabled_languages = detect_languages - app_store_version_localizations = verify_available_version_languages!(options, app, enabled_languages) unless options[:edit_live] + app_store_version_localizations = verify_available_version_languages!(app, enabled_languages) unless options[:edit_live] app_info = fetch_edit_app_info(app) - app_info_localizations = verify_available_info_languages!(options, app, app_info, enabled_languages) unless options[:edit_live] || !updating_localized_app_info?(options, app, app_info) + app_info_localizations = verify_available_info_languages!(app, app_info, enabled_languages) unless options[:edit_live] || !updating_localized_app_info?(app, app_info) if options[:edit_live] # not all values are editable when using live_version @@ -342,9 +348,9 @@ def upload(options) end end - set_review_information(version, options) - set_review_attachment_file(version, options) - set_app_rating(app_info, options) + review_information(version) + review_attachment_file(version) + app_rating(app_info) end # rubocop:enable Metrics/PerceivedComplexity @@ -360,12 +366,12 @@ def convert_ms_to_iso8601(time_in_ms) end # If the user is using the 'default' language, then assign values where they are needed - def assign_defaults(options) + def assign_defaults # Normalizes languages keys from symbols to strings - normalize_language_keys(options) + normalize_language_keys # Build a complete list of the required languages - enabled_languages = detect_languages(options) + enabled_languages = detect_languages # Get all languages used in existing settings (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key| @@ -402,7 +408,7 @@ def assign_defaults(options) end end - def detect_languages(options) + def detect_languages # Build a complete list of the required languages enabled_languages = options[:languages] || [] @@ -427,40 +433,49 @@ def detect_languages(options) .uniq end - def fetch_edit_app_store_version(app, platform, wait_time: 10) - retry_if_nil("Cannot find edit app store version", wait_time: wait_time) do + def fetch_edit_app_store_version(app, platform) + retry_if_nil("Cannot find edit app store version") do app.get_edit_app_store_version(platform: platform) end end - def fetch_edit_app_info(app, wait_time: 10) - retry_if_nil("Cannot find edit app info", wait_time: wait_time) do + def fetch_edit_app_info(app) + retry_if_nil("Cannot find edit app info") do app.fetch_edit_app_info end end - def fetch_live_app_info(app, wait_time: 10) - retry_if_nil("Cannot find live app info", wait_time: wait_time) do + def fetch_live_app_info(app) + retry_if_nil("Cannot find live app info") do app.fetch_live_app_info end end - def retry_if_nil(message, tries: 5, wait_time: 10) + # Retries a block of code if the return value is nil, with an exponential backoff. + def retry_if_nil(message) + tries = options[:version_check_wait_retry_limit] + wait_time = 10 loop do tries -= 1 value = yield return value if value - UI.message("#{message}... Retrying after #{wait_time} seconds (remaining: #{tries})") - sleep(wait_time) + # Calculate sleep time to be the lesser of the exponential backoff or 5 minutes. + # This prevents problems with CI's console output timeouts (of usually 10 minutes), and also + # speeds up the retry time for the user, as waiting longer than 5 minutes is a too long wait for a retry. + sleep_time = [wait_time * 2, 5 * 60].min + UI.message("#{message}... Retrying after #{sleep_time} seconds (remaining: #{tries})") + Kernel.sleep(sleep_time) return nil if tries.zero? + + wait_time *= 2 # Double the wait time for the next iteration end end # Checking if the metadata to update includes localised App Info - def updating_localized_app_info?(options, app, app_info) + def updating_localized_app_info?(app, app_info) app_info ||= fetch_live_app_info(app) unless app_info UI.important("Can't find edit or live App info. Skipping upload.") @@ -499,7 +514,7 @@ def updating_localized_app_info?(options, app, app_info) end # Finding languages to enable - def verify_available_info_languages!(options, app, app_info, languages) + def verify_available_info_languages!(app, app_info, languages) unless app_info UI.user_error!("Cannot update languages - could not find an editable 'App Info'. Verify that your app is in one of the editable states in App Store Connect") return @@ -531,7 +546,7 @@ def verify_available_info_languages!(options, app, app_info, languages) end # Finding languages to enable - def verify_available_version_languages!(options, app, languages) + def verify_available_version_languages!(app, languages) platform = Spaceship::ConnectAPI::Platform.map(options[:platform]) version = fetch_edit_app_store_version(app, platform) @@ -566,7 +581,7 @@ def verify_available_version_languages!(options, app, languages) end # Loads the metadata files and stores them into the options object - def load_from_filesystem(options) + def load_from_filesystem return if options[:skip_metadata] # Load localised data @@ -623,7 +638,7 @@ def load_from_filesystem(options) private # Normalizes languages keys from symbols to strings - def normalize_language_keys(options) + def normalize_language_keys (LOCALISED_VERSION_VALUES.keys + LOCALISED_APP_VALUES.keys).each do |key| current = options[key] next unless current && current.kind_of?(Hash) @@ -636,7 +651,7 @@ def normalize_language_keys(options) options end - def set_review_information(version, options) + def review_information(version) info = options[:app_review_information] return if info.nil? || info.empty? @@ -669,7 +684,7 @@ def set_review_information(version, options) end end - def set_review_attachment_file(version, options) + def review_attachment_file(version) app_store_review_detail = version.fetch_app_store_review_detail app_store_review_attachments = app_store_review_detail.app_store_review_attachments || [] @@ -687,7 +702,7 @@ def set_review_attachment_file(version, options) end end - def set_app_rating(app_info, options) + def app_rating(app_info) return unless options[:app_rating_config_path] require 'json' diff --git a/deliver/lib/deliver/upload_screenshots.rb b/deliver/lib/deliver/upload_screenshots.rb index 9046ce6f152..575e6e81892 100644 --- a/deliver/lib/deliver/upload_screenshots.rb +++ b/deliver/lib/deliver/upload_screenshots.rb @@ -55,7 +55,7 @@ def upload(options, screenshots) localizations = version.get_app_store_version_localizations end - upload_screenshots(localizations, screenshots_per_language) + upload_screenshots(localizations, screenshots_per_language, options[:screenshot_processing_timeout]) Helper.show_loading_indicator("Sorting screenshots uploaded...") sort_screenshots(localizations) @@ -109,7 +109,7 @@ def delete_screenshots(localizations, screenshots_per_language, tries: 5) end end - def upload_screenshots(localizations, screenshots_per_language, tries: 5) + def upload_screenshots(localizations, screenshots_per_language, timeout_seconds, tries: 5) tries -= 1 # Upload screenshots @@ -158,15 +158,16 @@ def upload_screenshots(localizations, screenshots_per_language, tries: 5) UI.verbose('Uploading jobs are completed') Helper.show_loading_indicator("Waiting for all the screenshots to finish being processed...") - states = wait_for_complete(iterator) + states = wait_for_complete(iterator, timeout_seconds) Helper.hide_loading_indicator - retry_upload_screenshots_if_needed(iterator, states, total_number_of_screenshots, tries, localizations, screenshots_per_language) + retry_upload_screenshots_if_needed(iterator, states, total_number_of_screenshots, tries, timeout_seconds, localizations, screenshots_per_language) UI.message("Successfully uploaded all screenshots") end # Verify all screenshots have been processed - def wait_for_complete(iterator) + def wait_for_complete(iterator, timeout_seconds) + start_time = Time.now loop do states = iterator.each_app_screenshot.map { |_, _, app_screenshot| app_screenshot }.each_with_object({}) do |app_screenshot, hash| state = app_screenshot.asset_delivery_state['state'] @@ -177,16 +178,22 @@ def wait_for_complete(iterator) is_processing = states.fetch('UPLOAD_COMPLETE', 0) > 0 return states unless is_processing + if Time.now - start_time > timeout_seconds + UI.important("Screenshot upload reached the timeout limit of #{timeout_seconds} seconds. We'll now retry uploading the screenshots that couldn't be uploaded in time.") + return states + end + UI.verbose("There are still incomplete screenshots - #{states}") sleep(5) end end # Verify all screenshots states on App Store Connect are okay - def retry_upload_screenshots_if_needed(iterator, states, number_of_screenshots, tries, localizations, screenshots_per_language) + def retry_upload_screenshots_if_needed(iterator, states, number_of_screenshots, tries, timeout_seconds, localizations, screenshots_per_language) is_failure = states.fetch("FAILED", 0) > 0 + is_processing = states.fetch('UPLOAD_COMPLETE', 0) > 0 is_missing_screenshot = !screenshots_per_language.empty? && !verify_local_screenshots_are_uploaded(iterator, screenshots_per_language) - return unless is_failure || is_missing_screenshot + return unless is_failure || is_missing_screenshot || is_processing if tries.zero? iterator.each_app_screenshot.select { |_, _, app_screenshot| app_screenshot.error? }.each do |localization, _, app_screenshot| @@ -200,7 +207,7 @@ def retry_upload_screenshots_if_needed(iterator, states, number_of_screenshots, iterator.each_app_screenshot do |_, _, app_screenshot| app_screenshot.delete! unless app_screenshot.complete? end - upload_screenshots(localizations, screenshots_per_language, tries: tries) + upload_screenshots(localizations, screenshots_per_language, timeout_seconds, tries: tries) end end diff --git a/deliver/spec/runner_spec.rb b/deliver/spec/runner_spec.rb index 7b620fdc382..617a8bcb273 100644 --- a/deliver/spec/runner_spec.rb +++ b/deliver/spec/runner_spec.rb @@ -30,7 +30,9 @@ def provider_ids let(:runner) do allow(Spaceship::ConnectAPI).to receive(:login).and_return(true) allow(Spaceship::ConnectAPI).to receive(:select_team).and_return(true) - allow(Spaceship::Tunes).to receive(:client).and_return(MockSession.new) + mock_session = MockSession.new + allow(Spaceship::Tunes).to receive(:client).and_return(mock_session) + allow(Spaceship::ConnectAPI).to receive_message_chain('client.tunes_client').and_return(mock_session) allow_any_instance_of(Deliver::DetectValues).to receive(:run!) { |opt| opt } Deliver::Runner.new(options) end @@ -46,6 +48,9 @@ def provider_ids } end + let(:fake_team_api_key_json_path) { File.absolute_path("./spaceship/spec/connect_api/fixtures/asc_key.json") } + let(:fake_individual_api_key_json_path) { File.absolute_path("./spaceship/spec/connect_api/fixtures/asc_individual_key.json") } + before do allow(Deliver).to receive(:cache).and_return({ app: double('app', { id: 'YI8C2AS' }) @@ -82,6 +87,20 @@ def provider_ids end end + describe 'with an IPA file for visionOS' do + before do + options[:platform] = 'xros' + end + + it 'uploads the IPA for the visionOS platform' do + expect_any_instance_of(FastlaneCore::IpaUploadPackageBuilder).to receive(:generate) + .with(app_id: 'YI8C2AS', ipa_path: 'ACME.ipa', package_path: '/tmp', platform: 'xros') + .and_return('path') + expect(transporter).to receive(:upload).with(package_path: 'path', asset_path: 'ACME.ipa', platform: 'xros').and_return(true) + runner.upload_binary + end + end + describe 'with a PKG file for macOS' do before do options[:platform] = 'osx' @@ -97,6 +116,67 @@ def provider_ids runner.upload_binary end end + + describe 'with Team API Key' do + before do + options[:api_key] = JSON.load_file(fake_team_api_key_json_path, symbolize_names: true) + end + + it 'initializes transporter with API key' do + token = instance_double(Spaceship::ConnectAPI::Token, { + text: 'API_TOKEN', + expired?: false + }) + allow(Spaceship::ConnectAPI).to receive(:token).and_return(token) + allow(Spaceship::ConnectAPI).to receive(:token=) + expect_any_instance_of(FastlaneCore::IpaUploadPackageBuilder).to receive(:generate).and_return('path') + expect(FastlaneCore::ItunesTransporter).to receive(:new) + .with( + nil, + nil, + false, + nil, + 'API_TOKEN', + { + altool_compatible_command: true, + api_key: options[:api_key], + } + ) + .and_return(transporter) + expect(transporter).to receive(:upload).with(package_path: 'path', asset_path: 'ACME.ipa', platform: 'ios').and_return(true) + runner.upload_binary + end + end + + describe 'with Individual API Key' do + before do + options[:api_key] = JSON.load_file(fake_individual_api_key_json_path, symbolize_names: true) + end + + it 'initializes transporter with username' do + token = instance_double(Spaceship::ConnectAPI::Token, { + text: 'API_TOKEN', + expired?: false + }) + allow(Spaceship::ConnectAPI).to receive(:token).and_return(token) + allow(Spaceship::ConnectAPI).to receive(:token=) + expect_any_instance_of(FastlaneCore::IpaUploadPackageBuilder).to receive(:generate).and_return('path') + expect(FastlaneCore::ItunesTransporter).to receive(:new) + .with( + 'bill@acme.com', + nil, + false, + nil, + { + altool_compatible_command: true, + api_key: nil, + } + ) + .and_return(transporter) + expect(transporter).to receive(:upload).and_return(true) + runner.upload_binary + end + end end describe :verify_binary do @@ -129,6 +209,20 @@ def provider_ids end end + describe 'with an IPA file for visionOS' do + before do + options[:platform] = 'xros' + end + + it 'verifies the IPA for the visionOS platform' do + expect_any_instance_of(FastlaneCore::IpaUploadPackageBuilder).to receive(:generate) + .with(app_id: 'YI8C2AS', ipa_path: 'ACME.ipa', package_path: '/tmp', platform: 'xros') + .and_return('path') + expect(transporter).to receive(:verify).with(asset_path: "ACME.ipa", package_path: 'path', platform: "xros").and_return(true) + runner.verify_binary + end + end + describe 'with a PKG file for macOS' do before do options[:platform] = 'osx' @@ -144,5 +238,66 @@ def provider_ids runner.verify_binary end end + + describe 'with Team API Key' do + before do + options[:api_key] = JSON.load_file(fake_team_api_key_json_path, symbolize_names: true) + end + + it 'initializes transporter with API Key' do + token = instance_double(Spaceship::ConnectAPI::Token, { + text: 'API_TOKEN', + expired?: false + }) + allow(Spaceship::ConnectAPI).to receive(:token).and_return(token) + allow(Spaceship::ConnectAPI).to receive(:token=) + allow_any_instance_of(FastlaneCore::IpaUploadPackageBuilder).to receive(:generate).and_return('path') + expect(FastlaneCore::ItunesTransporter).to receive(:new) + .with( + nil, + nil, + false, + nil, + 'API_TOKEN', + { + altool_compatible_command: true, + api_key: options[:api_key], + } + ) + .and_return(transporter) + expect(transporter).to receive(:verify).and_return(true) + runner.verify_binary + end + end + + describe 'with Individual API Key' do + before do + options[:api_key] = JSON.load_file(fake_individual_api_key_json_path, symbolize_names: true) + end + + it 'initializes transporter with username' do + token = instance_double(Spaceship::ConnectAPI::Token, { + text: 'API_TOKEN', + expired?: false + }) + allow(Spaceship::ConnectAPI).to receive(:token).and_return(token) + allow(Spaceship::ConnectAPI).to receive(:token=) + allow_any_instance_of(FastlaneCore::IpaUploadPackageBuilder).to receive(:generate).and_return('path') + expect(FastlaneCore::ItunesTransporter).to receive(:new) + .with( + 'bill@acme.com', + nil, + false, + nil, + { + altool_compatible_command: true, + api_key: nil, + } + ) + .and_return(transporter) + expect(transporter).to receive(:verify).and_return(true) + runner.verify_binary + end + end end end diff --git a/deliver/spec/submit_for_review_spec.rb b/deliver/spec/submit_for_review_spec.rb index fb960990094..ebaf4758f12 100644 --- a/deliver/spec/submit_for_review_spec.rb +++ b/deliver/spec/submit_for_review_spec.rb @@ -14,17 +14,16 @@ let(:ready_for_review_version) do double('ready_for_review_version', id: '1', - app_store_state: "READY_FOR_REVIEW", + app_version_state: "READY_FOR_REVIEW", version_string: "1.0.0") end let(:prepare_for_submission_version) do double('prepare_for_submission_version', id: '1', - app_store_state: "PREPARE_FOR_SUBMISSION", + app_version_state: "PREPARE_FOR_SUBMISSION", version_string: "1.0.0") end let(:selected_build) { double('selected_build') } - let(:idfa_declaration) { double('idfa_declaration') } let(:submission) do double('submission', @@ -66,26 +65,6 @@ review_submitter.submit!(options) end.to raise_error("boom") end - - it 'needs to set export_compliance_uses_encryption' do - options = { - platform: Spaceship::ConnectAPI::Platform::IOS - } - - expect(app).to receive(:get_edit_app_store_version).and_return(edit_version) - expect(review_submitter).to receive(:select_build).and_return(selected_build) - - expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(nil) - expect(edit_version).to receive(:uses_idfa).and_return(nil) - - expect(UI).to receive(:user_error!).with(/Use of Advertising Identifier \(IDFA\) is required to submit/).and_raise("boom") - - expect do - review_submitter.submit!(options) - end.to raise_error("boom") - end end context 'submits successfully' do @@ -101,9 +80,6 @@ expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(nil) - expect(edit_version).to receive(:uses_idfa).and_return(false) - expect(app).to receive(:get_in_progress_review_submission).and_return(submission) expect do @@ -121,9 +97,6 @@ expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(nil) - expect(edit_version).to receive(:uses_idfa).and_return(false) - expect(app).to receive(:get_in_progress_review_submission).and_return(nil) expect(app).to receive(:get_ready_review_submission).and_return(submission) expect(submission).to receive(:items).and_return([]) @@ -146,9 +119,6 @@ expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(nil) - expect(edit_version).to receive(:uses_idfa).and_return(false) - expect(app).to receive(:get_in_progress_review_submission).and_return(nil) expect(app).to receive(:get_ready_review_submission).and_return(submission) expect(submission).to receive(:items).and_return([double('some item')]) @@ -170,9 +140,6 @@ expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(nil) - expect(edit_version).to receive(:uses_idfa).and_return(false) - expect(app).to receive(:get_in_progress_review_submission).and_return(nil) expect(app).to receive(:get_ready_review_submission).and_return(submission) expect(submission).to receive(:items).and_return([]) @@ -203,9 +170,6 @@ expect(selected_build).to receive(:update).with(attributes: { usesNonExemptEncryption: false }).and_return(selected_build) expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(nil) - expect(edit_version).to receive(:uses_idfa).and_return(false) - expect(app).to receive(:get_in_progress_review_submission).and_return(nil) expect(app).to receive(:get_ready_review_submission).and_return(nil) expect(app).to receive(:create_review_submission).and_return(submission) @@ -232,9 +196,6 @@ expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(nil) - expect(edit_version).to receive(:uses_idfa).and_return(false) - expect(app).to receive(:update).with(attributes: { contentRightsDeclaration: "USES_THIRD_PARTY_CONTENT" }) @@ -250,145 +211,6 @@ review_submitter.submit!(options) end end - - context 'IDFA' do - it 'submission information with idfa false with no idfa' do - options = { - platform: Spaceship::ConnectAPI::Platform::IOS, - submission_information: { - add_id_info_uses_idfa: false - } - } - - expect(app).to receive(:get_edit_app_store_version).and_return(edit_version) - expect(review_submitter).to receive(:select_build).and_return(selected_build) - - expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(nil) - expect(edit_version).to receive(:update).with(attributes: { usesIdfa: false }).and_return(edit_version) - expect(edit_version).to receive(:uses_idfa).and_return(false).exactly(2).times - - expect(app).to receive(:get_in_progress_review_submission).and_return(nil) - expect(app).to receive(:get_ready_review_submission).and_return(nil) - expect(app).to receive(:create_review_submission).and_return(submission) - - expect(submission).to receive(:add_app_store_version_to_review_items).with(app_store_version_id: edit_version.id) - expect(Spaceship::ConnectAPI::AppStoreVersion).to receive(:get).and_return(ready_for_review_version) - expect(submission).to receive(:submit_for_review) - - review_submitter.submit!(options) - end - - it 'submission information with idfa false with existing idfa' do - options = { - platform: Spaceship::ConnectAPI::Platform::IOS, - submission_information: { - add_id_info_uses_idfa: false - } - } - - expect(app).to receive(:get_edit_app_store_version).and_return(edit_version) - expect(review_submitter).to receive(:select_build).and_return(selected_build) - - expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(idfa_declaration) - expect(edit_version).to receive(:update).with(attributes: { usesIdfa: false }).and_return(edit_version) - expect(edit_version).to receive(:uses_idfa).and_return(false).exactly(2).times - expect(idfa_declaration).to receive(:delete!) - - expect(app).to receive(:get_in_progress_review_submission).and_return(nil) - expect(app).to receive(:get_ready_review_submission).and_return(nil) - expect(app).to receive(:create_review_submission).and_return(submission) - - expect(submission).to receive(:add_app_store_version_to_review_items).with(app_store_version_id: edit_version.id) - expect(Spaceship::ConnectAPI::AppStoreVersion).to receive(:get).and_return(ready_for_review_version) - expect(submission).to receive(:submit_for_review) - - review_submitter.submit!(options) - end - - it 'submission information with idfa true with no idfa' do - options = { - platform: Spaceship::ConnectAPI::Platform::IOS, - submission_information: { - add_id_info_uses_idfa: true, - - add_id_info_limits_tracking: true, - add_id_info_serves_ads: true, - add_id_info_tracks_install: true, - add_id_info_tracks_action: true - } - } - - expect(app).to receive(:get_edit_app_store_version).and_return(edit_version) - expect(review_submitter).to receive(:select_build).and_return(selected_build) - - expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(nil) - expect(edit_version).to receive(:update).with(attributes: { usesIdfa: true }).and_return(edit_version) - expect(edit_version).to receive(:uses_idfa).and_return(true).exactly(2).times - - expect(edit_version).to receive(:create_idfa_declaration).with(attributes: { - honorsLimitedAdTracking: true, - servesAds: true, - attributesAppInstallationToPreviousAd: true, - attributesActionWithPreviousAd: true - }) - - expect(app).to receive(:get_in_progress_review_submission).and_return(nil) - expect(app).to receive(:get_ready_review_submission).and_return(nil) - expect(app).to receive(:create_review_submission).and_return(submission) - - expect(submission).to receive(:add_app_store_version_to_review_items).with(app_store_version_id: edit_version.id) - expect(Spaceship::ConnectAPI::AppStoreVersion).to receive(:get).and_return(ready_for_review_version) - expect(submission).to receive(:submit_for_review) - - review_submitter.submit!(options) - end - - it 'submission information with idfa true with existing idfa' do - options = { - platform: Spaceship::ConnectAPI::Platform::IOS, - submission_information: { - add_id_info_uses_idfa: true, - - add_id_info_limits_tracking: true, - add_id_info_serves_ads: true, - add_id_info_tracks_install: true, - add_id_info_tracks_action: true - } - } - - expect(app).to receive(:get_edit_app_store_version).and_return(edit_version) - expect(review_submitter).to receive(:select_build).and_return(selected_build) - - expect(selected_build).to receive(:uses_non_exempt_encryption).and_return(false) - - expect(edit_version).to receive(:fetch_idfa_declaration).and_return(idfa_declaration) - expect(edit_version).to receive(:update).with(attributes: { usesIdfa: true }).and_return(edit_version) - expect(edit_version).to receive(:uses_idfa).and_return(true).exactly(2).times - - expect(idfa_declaration).to receive(:update).with(attributes: { - honorsLimitedAdTracking: true, - servesAds: true, - attributesAppInstallationToPreviousAd: true, - attributesActionWithPreviousAd: true - }) - - expect(app).to receive(:get_in_progress_review_submission).and_return(nil) - expect(app).to receive(:get_ready_review_submission).and_return(nil) - expect(app).to receive(:create_review_submission).and_return(submission) - - expect(submission).to receive(:add_app_store_version_to_review_items).with(app_store_version_id: edit_version.id) - expect(Spaceship::ConnectAPI::AppStoreVersion).to receive(:get).and_return(ready_for_review_version) - expect(submission).to receive(:submit_for_review) - - review_submitter.submit!(options) - end - end end end end diff --git a/deliver/spec/upload_metadata_spec.rb b/deliver/spec/upload_metadata_spec.rb index a9183547eed..51a3dbb5f51 100644 --- a/deliver/spec/upload_metadata_spec.rb +++ b/deliver/spec/upload_metadata_spec.rb @@ -2,12 +2,12 @@ require 'tempfile' describe Deliver::UploadMetadata do - let(:uploader) { Deliver::UploadMetadata.new } let(:tmpdir) { Dir.mktmpdir } describe '#load_from_filesystem' do context 'with review information' do let(:options) { { metadata_path: tmpdir, app_review_information: app_review_information } } + let(:uploader) { Deliver::UploadMetadata.new(options) } def create_metadata(path, text) File.open(File.join(path), 'w') do |f| @@ -33,7 +33,7 @@ def create_metadata(path, text) context 'without app_review_information' do let(:app_review_information) { nil } it 'can load review information from file' do - uploader.load_from_filesystem(options) + uploader.load_from_filesystem expect(options[:app_review_information][:first_name]).to eql('Alice') expect(options[:app_review_information][:last_name]).to eql('Smith') expect(options[:app_review_information][:phone_number]).to eql('+819012345678') @@ -47,7 +47,7 @@ def create_metadata(path, text) context 'with app_review_information' do let(:app_review_information) { { notes: 'This is a note from option' } } it 'values will be masked by the in options' do - uploader.load_from_filesystem(options) + uploader.load_from_filesystem expect(options[:app_review_information][:first_name]).to eql('Alice') expect(options[:app_review_information][:last_name]).to eql('Smith') expect(options[:app_review_information][:phone_number]).to eql('+819012345678') @@ -64,10 +64,11 @@ def create_metadata(path, text) end end - describe "#set_review_information" do + describe "#review_information" do let(:options) { { metadata_path: tmpdir, app_review_information: app_review_information } } let(:version) { double("version") } let(:app_store_review_detail) { double("app_store_review_detail") } + let(:uploader) { Deliver::UploadMetadata.new(options) } context "with review_information" do let(:app_review_information) do @@ -81,8 +82,10 @@ def create_metadata(path, text) end it "skips review information with empty app_review_information" do + options[:app_review_information] = {} + expect(FastlaneCore::UI).not_to receive(:message).with("Uploading app review information to App Store Connect") - uploader.send("set_review_information", version, { app_review_information: {} }) + uploader.send("review_information", version) end it "successfully set review information" do @@ -100,7 +103,7 @@ def create_metadata(path, text) expect(FastlaneCore::UI).to receive(:message).with("Uploading app review information to App Store Connect") - uploader.send("set_review_information", version, options) + uploader.send("review_information", version) end end @@ -116,7 +119,7 @@ def create_metadata(path, text) "demo_account_required" => true }) - uploader.send("set_review_information", version, options) + uploader.send("review_information", version) end end @@ -129,7 +132,7 @@ def create_metadata(path, text) "demo_account_required" => false }) - uploader.send("set_review_information", version, options) + uploader.send("review_information", version) end end @@ -142,7 +145,7 @@ def create_metadata(path, text) "demo_account_required" => false }) - uploader.send("set_review_information", version, options) + uploader.send("review_information", version) end end end @@ -175,6 +178,10 @@ def create_metadata(path, text) count: 0) end + let(:options) { { version_check_wait_retry_limit: 5 } } + + let(:uploader) { Deliver::UploadMetadata.new(options) } + let(:metadata_path) { Dir.mktmpdir } context "fetch app edit" do @@ -182,22 +189,24 @@ def create_metadata(path, text) it "no retry" do expect(app).to receive(:get_edit_app_store_version).and_return(version) - edit_version = uploader.fetch_edit_app_store_version(app, 'IOS', wait_time: 0.1) + edit_version = uploader.fetch_edit_app_store_version(app, 'IOS') expect(edit_version).to eq(version) end it "1 retry" do + expect(Kernel).to receive(:sleep).once.with(20) expect(app).to receive(:get_edit_app_store_version).and_return(nil) expect(app).to receive(:get_edit_app_store_version).and_return(version) - edit_version = uploader.fetch_edit_app_store_version(app, 'IOS', wait_time: 0.1) + edit_version = uploader.fetch_edit_app_store_version(app, 'IOS') expect(edit_version).to eq(version) end it "5 retry" do + expect(Kernel).to receive(:sleep).exactly(5).times expect(app).to receive(:get_edit_app_store_version).and_return(nil).exactly(5).times - edit_version = uploader.fetch_edit_app_store_version(app, 'IOS', wait_time: 0.1) + edit_version = uploader.fetch_edit_app_store_version(app, 'IOS') expect(edit_version).to eq(nil) end end @@ -206,34 +215,38 @@ def create_metadata(path, text) it "no retry" do expect(app).to receive(:fetch_edit_app_info).and_return(app_info) - edit_app_info = uploader.fetch_edit_app_info(app, wait_time: 0.1) + edit_app_info = uploader.fetch_edit_app_info(app) expect(edit_app_info).to eq(app_info) end it "1 retry" do + expect(Kernel).to receive(:sleep).once.with(20) expect(app).to receive(:fetch_edit_app_info).and_return(nil) expect(app).to receive(:fetch_edit_app_info).and_return(app_info) - edit_app_info = uploader.fetch_edit_app_info(app, wait_time: 0.1) + edit_app_info = uploader.fetch_edit_app_info(app) expect(edit_app_info).to eq(app_info) end it "5 retry" do + expect(Kernel).to receive(:sleep).exactly(5).times expect(app).to receive(:fetch_edit_app_info).and_return(nil).exactly(5).times - edit_app_info = uploader.fetch_edit_app_info(app, wait_time: 0.1) + edit_app_info = uploader.fetch_edit_app_info(app) expect(edit_app_info).to eq(nil) end end end context "#upload" do + let(:options) { {} } + before do allow(Deliver).to receive(:cache).and_return({ app: app }) - allow(uploader).to receive(:set_review_information) - allow(uploader).to receive(:set_review_attachment_file) - allow(uploader).to receive(:set_app_rating) + allow(uploader).to receive(:review_information) + allow(uploader).to receive(:review_attachment_file) + allow(uploader).to receive(:app_rating) # Verify available languages expect(app).to receive(:id).and_return(id) @@ -254,12 +267,11 @@ def create_metadata(path, text) context "normal metadata" do it "saves metadata" do - options = { - platform: "ios", - metadata_path: metadata_path, - name: { "en-US" => "App name" }, - description: { "en-US" => "App description" } - } + options[:platform] = "ios" + options[:metadata_path] = metadata_path + options[:name] = { "en-US" => "App name" } + options[:description] = { "en-US" => "App description" } + options[:version_check_wait_retry_limit] = 5 # Get number of versions (used for if whats_new should be sent) expect(Spaceship::ConnectAPI).to receive(:get_app_store_versions).and_return(app_store_versions) @@ -285,18 +297,17 @@ def create_metadata(path, text) # Update app info expect(app_info).to receive(:update_categories).with(category_id_map: {}) - uploader.upload(options) + uploader.upload end end context "with privacy_url" do it 'saves privacy_url' do - options = { - platform: "ios", - metadata_path: metadata_path, - privacy_url: { "en-US" => "https://fastlane.tools" }, - apple_tv_privacy_policy: { "en-US" => "https://fastlane.tools/tv" } - } + options[:platform] = "ios" + options[:metadata_path] = metadata_path + options[:privacy_url] = { "en-US" => "https://fastlane.tools" } + options[:apple_tv_privacy_policy] = { "en-US" => "https://fastlane.tools/tv" } + options[:version_check_wait_retry_limit] = 5 # Get number of versions (used for if whats_new should be sent) expect(Spaceship::ConnectAPI).to receive(:get_app_store_versions).and_return(app_store_versions) @@ -310,17 +321,16 @@ def create_metadata(path, text) # Update app info expect(app_info).to receive(:update_categories).with(category_id_map: {}) - uploader.upload(options) + uploader.upload end end context "with auto_release_date" do it 'with date' do - options = { - platform: "ios", - metadata_path: metadata_path, - auto_release_date: 1_595_395_800_000 - } + options[:platform] = "ios" + options[:metadata_path] = metadata_path + options[:auto_release_date] = 1_595_395_800_000 + options[:version_check_wait_retry_limit] = 5 # Get number of version (used for if whats_new should be sent) expect(Spaceship::ConnectAPI).to receive(:get_app_store_versions).and_return(app_store_versions) @@ -333,18 +343,17 @@ def create_metadata(path, text) # Update app info expect(app_info).to receive(:update_categories).with(category_id_map: {}) - uploader.upload(options) + uploader.upload end end context "with phased_release" do it 'with true' do - options = { - platform: "ios", - metadata_path: metadata_path, - phased_release: true, - automatic_release: false - } + options[:platform] = "ios" + options[:metadata_path] = metadata_path + options[:phased_release] = true + options[:automatic_release] = false + options[:version_check_wait_retry_limit] = 5 # Get number of version (used for if whats_new should be sent) expect(Spaceship::ConnectAPI).to receive(:get_app_store_versions).and_return(app_store_versions) @@ -365,15 +374,14 @@ def create_metadata(path, text) # Update app info expect(app_info).to receive(:update_categories).with(category_id_map: {}) - uploader.upload(options) + uploader.upload end it 'with false' do - options = { - platform: "ios", - metadata_path: metadata_path, - phased_release: false - } + options[:platform] = "ios" + options[:metadata_path] = metadata_path + options[:phased_release] = false + options[:version_check_wait_retry_limit] = 5 # Get number of version (used for if whats_new should be sent) expect(Spaceship::ConnectAPI).to receive(:get_app_store_versions).and_return(app_store_versions) @@ -391,17 +399,16 @@ def create_metadata(path, text) # Update app info expect(app_info).to receive(:update_categories).with(category_id_map: {}) - uploader.upload(options) + uploader.upload end end context "with reset_ratings" do - it 'with true' do - options = { - platform: "ios", - metadata_path: metadata_path, - reset_ratings: true - } + it 'with select reset_ratings' do + options[:platform] = "ios" + options[:metadata_path] = metadata_path + options[:reset_ratings] = true + options[:version_check_wait_retry_limit] = 5 # Get number of version (used for if whats_new should be sent) expect(Spaceship::ConnectAPI).to receive(:get_app_store_versions).and_return(app_store_versions) @@ -418,15 +425,14 @@ def create_metadata(path, text) # Update app info expect(app_info).to receive(:update_categories).with(category_id_map: {}) - uploader.upload(options) + uploader.upload end - it 'with false' do - options = { - platform: "ios", - metadata_path: metadata_path, - reset_ratings: false - } + it 'does not select reset_ratings' do + options[:platform] = "ios" + options[:metadata_path] = metadata_path + options[:reset_ratings] = false + options[:version_check_wait_retry_limit] = 5 # Get number of version (used for if whats_new should be sent) expect(Spaceship::ConnectAPI).to receive(:get_app_store_versions).and_return(app_store_versions) @@ -444,18 +450,18 @@ def create_metadata(path, text) # Update app info expect(app_info).to receive(:update_categories).with(category_id_map: {}) - uploader.upload(options) + uploader.upload end end context "with no editable app info" do let(:live_app_info) { double('app_info') } let(:app_info) { nil } + it 'no new app info provided by user' do - options = { - platform: "ios", - metadata_path: metadata_path, - } + options[:platform] = "ios" + options[:metadata_path] = metadata_path + options[:version_check_wait_retry_limit] = 5 # Get live app info expect(app).to receive(:fetch_live_app_info).and_return(live_app_info) @@ -464,15 +470,14 @@ def create_metadata(path, text) expect(Spaceship::ConnectAPI).to receive(:get_app_store_versions).and_return(app_store_versions) expect(version).to receive(:update).with(attributes: {}) - uploader.upload(options) + uploader.upload end it 'same app info as live version' do - options = { - platform: "ios", - metadata_path: metadata_path, - name: { "en-US" => "App name" } - } + options[:platform] = "ios" + options[:metadata_path] = metadata_path + options[:name] = { "en-US" => "App name" } + options[:version_check_wait_retry_limit] = 5 # Get live app info expect(app).to receive(:fetch_live_app_info).and_return(live_app_info) @@ -485,7 +490,7 @@ def create_metadata(path, text) # Get app info localization in English (used to compare with data to upload) expect(app_info_localization_en).to receive(:name).and_return('App name') - uploader.upload(options) + uploader.upload end end end @@ -493,18 +498,20 @@ def create_metadata(path, text) context "fail when not allowed to update" do let(:live_app_info) { double('app_info') } let(:app_info) { nil } - it 'different app info than live version' do - options = { - platform: "ios", - metadata_path: metadata_path, - name: { "en-US" => "New app name" } + let(:options) { + { + platform: "ios", + metadata_path: metadata_path, + name: { "en-US" => "New app name" }, + version_check_wait_retry_limit: 5, } - + } + it 'different app info than live version' do allow(Deliver).to receive(:cache).and_return({ app: app }) - allow(uploader).to receive(:set_review_information) - allow(uploader).to receive(:set_review_attachment_file) - allow(uploader).to receive(:set_app_rating) + allow(uploader).to receive(:review_information) + allow(uploader).to receive(:review_attachment_file) + allow(uploader).to receive(:app_rating) # Get app info expect(uploader).to receive(:fetch_edit_app_info).and_return(app_info) @@ -522,13 +529,14 @@ def create_metadata(path, text) expect(FastlaneCore::UI).to receive(:user_error!).with("Cannot update languages - could not find an editable 'App Info'. Verify that your app is in one of the editable states in App Store Connect").and_call_original # Get app info localization in English (used to compare with data to upload) - expect { uploader.upload(options) }.to raise_error(FastlaneCore::Interface::FastlaneError) + expect { uploader.upload }.to raise_error(FastlaneCore::Interface::FastlaneError) end end end describe "#languages" do let(:options) { { metadata_path: tmpdir } } + let(:uploader) { Deliver::UploadMetadata.new(options) } def create_metadata(path, text) File.open(File.join(path), 'w') do |f| @@ -549,8 +557,8 @@ def create_filesystem_language(name) create_filesystem_language('de-DE') create_filesystem_language('el') - uploader.load_from_filesystem(options) - languages = uploader.detect_languages(options) + uploader.load_from_filesystem + languages = uploader.detect_languages expect(languages.sort).to eql(['de-DE', 'el', 'en-US']) end @@ -561,8 +569,8 @@ def create_filesystem_language(name) create_filesystem_language('default') create_filesystem_language('en-US') - uploader.load_from_filesystem(options) - languages = uploader.detect_languages(options) + uploader.load_from_filesystem + languages = uploader.detect_languages expect(languages.sort).to eql(['default', 'en-US']) end @@ -572,8 +580,8 @@ def create_filesystem_language(name) it "languages are 'en-AU', 'en-CA', 'en-GB'" do options[:languages] = ['en-AU', 'en-CA', 'en-GB'] - uploader.load_from_filesystem(options) - languages = uploader.detect_languages(options) + uploader.load_from_filesystem + languages = uploader.detect_languages expect(languages.sort).to eql(['en-AU', 'en-CA', 'en-GB']) end @@ -588,8 +596,8 @@ def create_filesystem_language(name) 'es-MX' => 'something else' } - uploader.load_from_filesystem(options) - languages = uploader.detect_languages(options) + uploader.load_from_filesystem + languages = uploader.detect_languages expect(languages.sort).to eql(['default', 'es-MX']) end @@ -606,8 +614,8 @@ def create_filesystem_language(name) 'something' ) - uploader.load_from_filesystem(options) - languages = uploader.detect_languages(options) + uploader.load_from_filesystem + languages = uploader.detect_languages expect(languages.sort).to eql(['default', 'en-US']) end @@ -626,8 +634,8 @@ def create_filesystem_language(name) create_filesystem_language('de-DE') create_filesystem_language('el') - uploader.load_from_filesystem(options) - languages = uploader.detect_languages(options) + uploader.load_from_filesystem + languages = uploader.detect_languages expect(languages.sort).to eql(['de-DE', 'default', 'el', 'en-AU', 'en-CA', 'en-GB', 'en-US', 'es-MX']) end @@ -646,8 +654,8 @@ def create_filesystem_language(name) create_filesystem_language('de-DE') create_filesystem_language('el') - uploader.load_from_filesystem(options) - uploader.assign_defaults(options) + uploader.load_from_filesystem + uploader.assign_defaults expect(options[:release_notes]["en-US"]).to eql('something else') expect(options[:release_notes]["es-MX"]).to eql('something else else') diff --git a/deliver/spec/upload_screenshots_spec.rb b/deliver/spec/upload_screenshots_spec.rb index bcdbfda144b..27e281d33f3 100644 --- a/deliver/spec/upload_screenshots_spec.rb +++ b/deliver/spec/upload_screenshots_spec.rb @@ -79,7 +79,7 @@ allow(described_class).to receive(:calculate_checksum).and_return('checksum') expect(app_screenshot_set).to receive(:upload_screenshot).with(path: local_screenshot.path, wait_for_processing: false) - subject.upload_screenshots([localization], screenshots_per_language) + subject.upload_screenshots([localization], screenshots_per_language, 3600) end end @@ -100,7 +100,7 @@ allow(FastlaneCore::Helper).to receive(:show_loading_indicator).and_return(true) expect(app_screenshot_set).to receive(:upload_screenshot).with(path: local_screenshot.path, wait_for_processing: false) - subject.upload_screenshots([localization], screenshots_per_language) + subject.upload_screenshots([localization], screenshots_per_language, 3600) end end end @@ -122,7 +122,7 @@ allow(FastlaneCore::Helper).to receive(:show_loading_indicator).and_return(true) expect(app_screenshot_set).to receive(:upload_screenshot).with(path: local_screenshot.path, wait_for_processing: false) - subject.upload_screenshots([localization], screenshots_per_language) + subject.upload_screenshots([localization], screenshots_per_language, 3600) end end @@ -145,7 +145,7 @@ allow(FastlaneCore::Helper).to receive(:show_loading_indicator).and_return(true) expect(app_screenshot_set).to_not(receive(:upload_screenshot)) - subject.upload_screenshots([localization], screenshots_per_language) + subject.upload_screenshots([localization], screenshots_per_language, 3600) end end @@ -166,7 +166,7 @@ allow(FastlaneCore::Helper).to receive(:show_loading_indicator).and_return(true) expect(app_screenshot_set).to receive(:upload_screenshot).exactly(10).times - subject.upload_screenshots([localization], screenshots_per_language) + subject.upload_screenshots([localization], screenshots_per_language, 3600) end end @@ -194,7 +194,7 @@ allow(FastlaneCore::Helper).to receive(:show_loading_indicator).and_return(true) expect(app_screenshot_set).to_not(receive(:upload_screenshot)) - subject.upload_screenshots([localization], screenshots_per_language) + subject.upload_screenshots([localization], screenshots_per_language, 3600) end end @@ -224,7 +224,7 @@ allow(FastlaneCore::Helper).to receive(:show_loading_indicator).and_return(true) expect(app_screenshot_set).to_not(receive(:upload_screenshot)) - subject.upload_screenshots([localization], screenshots_per_language) + subject.upload_screenshots([localization], screenshots_per_language, 3600) end end end @@ -242,8 +242,10 @@ get_app_screenshot_sets: [app_screenshot_set]) iterator = Deliver::AppScreenshotIterator.new([localization]) + allow(Time).to receive(:now).and_return(0) + expect(::Kernel).to_not(receive(:sleep)) - expect(subject.wait_for_complete(iterator)).to eq('COMPLETE' => 1) + expect(subject.wait_for_complete(iterator, 3600)).to eq('COMPLETE' => 1) end end @@ -258,9 +260,29 @@ get_app_screenshot_sets: [app_screenshot_set]) iterator = Deliver::AppScreenshotIterator.new([localization]) + allow(Time).to receive(:now).and_return(0) + expect_any_instance_of(Object).to receive(:sleep).with(kind_of(Numeric)).once expect(app_screenshot).to receive(:asset_delivery_state).and_return({ 'state' => 'UPLOAD_COMPLETE' }, { 'state' => 'COMPLETE' }) - expect(subject.wait_for_complete(iterator)).to eq('COMPLETE' => 1) + expect(subject.wait_for_complete(iterator, 3600)).to eq('COMPLETE' => 1) + end + end + + context 'when timeout is exceeded' do + it 'should exit the loop and return the current states' do + app_screenshot = double('Spaceship::ConnectAPI::AppScreenshot', asset_delivery_state: { 'state' => 'UPLOAD_COMPLETE' }) + app_screenshot_set = double('Spaceship::ConnectAPI::AppScreenshotSet', + screenshot_display_type: Spaceship::ConnectAPI::AppScreenshotSet::DisplayType::APP_IPHONE_55, + app_screenshots: [app_screenshot]) + localization = double('Spaceship::ConnectAPI::AppStoreVersionLocalization', + locale: 'en-US', + get_app_screenshot_sets: [app_screenshot_set]) + iterator = Deliver::AppScreenshotIterator.new([localization]) + states = { 'UPLOAD_COMPLETE' => 1 } + + allow(Time).to receive(:now).and_return(0, 3601) + + expect(subject.wait_for_complete(iterator, 3600)).to eq(states) end end end @@ -279,7 +301,7 @@ states = { 'FAILD' => 0, 'COMPLETE' => 1 } expect(subject).to_not(receive(:upload_screenshots)) - subject.retry_upload_screenshots_if_needed(iterator, states, 1, 1, [], {}) + subject.retry_upload_screenshots_if_needed(iterator, states, 1, 1, 0, [], {}) end end @@ -297,7 +319,7 @@ expect(subject).to receive(:upload_screenshots).with(any_args) expect(app_screenshot).to receive(:delete!) - subject.retry_upload_screenshots_if_needed(iterator, states, 1, 1, [], {}) + subject.retry_upload_screenshots_if_needed(iterator, states, 1, 1, 0, [], {}) end end @@ -314,7 +336,7 @@ states = { 'FAILED' => 1, 'COMPLETE' => 0 } expect(subject).to receive(:upload_screenshots).with(any_args) - subject.retry_upload_screenshots_if_needed(iterator, states, 999, 1, [], {}) + subject.retry_upload_screenshots_if_needed(iterator, states, 999, 1, 0, [], {}) end end @@ -336,7 +358,7 @@ expect(subject).to_not(receive(:upload_screenshots).with(any_args)) expect(UI).to receive(:user_error!) - subject.retry_upload_screenshots_if_needed(iterator, states, 1, 0, [], {}) + subject.retry_upload_screenshots_if_needed(iterator, states, 1, 0, 0, [], {}) end end @@ -357,7 +379,30 @@ expect(subject).to_not(receive(:upload_screenshots).with(any_args)) expect(UI).to_not(receive(:user_error!)) - subject.retry_upload_screenshots_if_needed(iterator, states, number_of_screenshots, 0, [], screenshots_per_language) + subject.retry_upload_screenshots_if_needed(iterator, states, number_of_screenshots, 0, 0, [], screenshots_per_language) + end + end + + context 'when screenshots are in UPLOAD_COMPLETE state' do + it 'should trigger retry logic' do + app_screenshot = double('Spaceship::ConnectAPI::AppScreenshot', + 'complete?' => false, + 'error?' => false, + file_name: '5.5_1.jpg', + error_messages: ['error_message']) + app_screenshot_set = double('Spaceship::ConnectAPI::AppScreenshotSet', + screenshot_display_type: Spaceship::ConnectAPI::AppScreenshotSet::DisplayType::APP_IPHONE_55, + app_screenshots: [app_screenshot]) + localization = double('Spaceship::ConnectAPI::AppStoreVersionLocalization', + locale: 'en-US', + get_app_screenshot_sets: [app_screenshot_set]) + localizations = [localization] + iterator = Deliver::AppScreenshotIterator.new(localizations) + states = { 'UPLOAD_COMPLETE' => 1 } + + expect(subject).to receive(:upload_screenshots).with(any_args) + expect(app_screenshot).to receive(:delete!) + subject.retry_upload_screenshots_if_needed(iterator, states, 1, 1, 0, localizations, {}) end end end diff --git a/fastlane.gemspec b/fastlane.gemspec index 1a770ba5a20..2c33a24b36e 100644 --- a/fastlane.gemspec +++ b/fastlane.gemspec @@ -22,35 +22,35 @@ Gem::Specification.new do |spec| spec.name = "fastlane" spec.version = Fastlane::VERSION # list of authors is regenerated and resorted on each release - spec.authors = ["Matthew Ellis", + spec.authors = ["Satoshi Namai", + "Manu Wallner", + "Felix Krause", + "Kohki Miki", + "Max Ott", + "Jorge Revuelta H", + "Jérôme Lacoste", + "Aaron Brager", + "Olivier Halligon", "Jimmy Dee", + "Manish Rathi", + "Iulian Onofrei", "Stefan Natchev", + "Maksym Grebenets", "Roger Oba", - "Kohki Miki", "Fumiya Nakamura", + "Matthew Ellis", + "Łukasz Grabowski", + "Danielle Tomlinson", "Josh Holtz", - "Iulian Onofrei", + "Daniel Jankowski", "Andrew McBurney", - "Maksym Grebenets", - "Manu Wallner", - "Jérôme Lacoste", - "Felix Krause", - "Łukasz Grabowski", + "Joshua Liebowitz", "Jan Piotrowski", - "Max Ott", "Helmut Januschka", - "Joshua Liebowitz", - "Danielle Tomlinson", - "Daniel Jankowski", - "Olivier Halligon", - "Satoshi Namai", - "Luka Mirosevic", - "Jorge Revuelta H", - "Manish Rathi", - "Aaron Brager"] + "Luka Mirosevic"] spec.email = ["fastlane@krausefx.com"] - spec.summary = Fastlane::DESCRIPTION + spec.summary = Fastlane::SUMMARY spec.description = Fastlane::DESCRIPTION spec.homepage = "https://fastlane.tools" spec.license = "MIT" @@ -75,7 +75,7 @@ Gem::Specification.new do |spec| spec.add_dependency('babosa', '>= 1.0.3', '< 2.0.0') # library for creating human-friendly identifiers, aka "slugs" spec.add_dependency('bundler', '>= 1.12.0', '< 3.0.0') # Used for fastlane plugins spec.add_dependency('CFPropertyList', '>= 2.3', '< 4.0.0') # Needed to be able to read binary plist format - spec.add_dependency('colored') # colored terminal output + spec.add_dependency('colored', '~> 1.2') # colored terminal output spec.add_dependency('commander', '~> 4.6') # CLI parser spec.add_dependency('dotenv', '>= 2.1.1', '< 3.0.0') spec.add_dependency('emoji_regex', '>= 0.1', '< 4.0') # Used to scan for Emoji in the changelog @@ -96,10 +96,10 @@ Gem::Specification.new do |spec| spec.add_dependency('mini_magick', '>= 4.9.4', '< 5.0.0') # To open, edit and export PSD files spec.add_dependency('multipart-post', '>= 2.0.0', '< 3.0.0') # Needed for uploading builds to appetize spec.add_dependency('naturally', '~> 2.2') # Used to sort strings with numbers in a human-friendly way - spec.add_dependency('optparse', '>= 0.1.1') # Used to parse options with Commander + spec.add_dependency('optparse', '>= 0.1.1', '< 1.0.0') # Used to parse options with Commander spec.add_dependency('plist', '>= 3.1.0', '< 4.0.0') # Needed for set_build_number_repository and get_info_plist_value actions spec.add_dependency('rubyzip', '>= 2.0.0', '< 3.0.0') # fix swift/ipa in gym - spec.add_dependency('security', '= 0.1.3') # macOS Keychain manager, a dead project, no updates expected + spec.add_dependency('security', '= 0.1.5') # macOS Keychain manager, a dead project, no updates expected spec.add_dependency('simctl', '~> 1.6.3') # Used for querying and interacting with iOS simulators spec.add_dependency('terminal-notifier', '>= 2.0.0', '< 3.0.0') # macOS notifications spec.add_dependency('terminal-table', '~> 3') # Actions documentation @@ -107,6 +107,6 @@ Gem::Specification.new do |spec| spec.add_dependency('tty-spinner', '>= 0.8.0', '< 1.0.0') # loading indicators spec.add_dependency('word_wrap', '~> 1.0.0') # to add line breaks for tables with long strings spec.add_dependency('xcodeproj', '>= 1.13.0', '< 2.0.0') # Modify Xcode projects - spec.add_dependency('xcpretty-travis-formatter', '>= 0.0.3') + spec.add_dependency('xcpretty-travis-formatter', '>= 0.0.3', '< 2.0.0') spec.add_dependency('xcpretty', '~> 0.3.0') # prettify xcodebuild output end diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d513195041f..33bd9b9dacf 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -102,15 +102,23 @@ end desc "Increment the version number of this gem, after generating new Swift API" lane :bump do |options| - verify_env_variables + bump_type = options[:bump_type] + is_automated_bump = !bump_type.nil? + + verify_env_variables(skip_ruby_gems: is_automated_bump) ensure_git_branch(branch: "master") ensure_git_status_clean github_api_token = ENV["FL_GITHUB_RELEASE_API_TOKEN"] - UI.user_error!("Please provide a GitHub API token using `FL_GITHUB_RELEASE_API_TOKEN`") if github_api_token.to_s.length == 0 + github_api_bearer = ENV["FL_GITHUB_RELEASE_API_BEARER"] + + if github_api_token.to_s.length == 0 && github_api_bearer.to_s.length == 0 + UI.user_error!("Please provide a GitHub API token using `FL_GITHUB_RELEASE_API_TOKEN` or `FL_GITHUB_RELEASE_API_BEARER`") + end paths_for_commit = [] version_file_path = "./fastlane/lib/fastlane/version.rb" + last_version_file_path = File.absolute_path("../CHANGELOG.latest.md") # Verify everything is in a consistent state latest_version = current_version @@ -119,6 +127,9 @@ lane :bump do |options| changelog_text = show_changelog + # Update CHANGELOG.lasest.md (which will be used to create the Release) + File.write(last_version_file_path, changelog_text) + bump_type ||= 'minor' if prompt(text: "New feature, method or API?", boolean: true) bump_type ||= 'patch' @@ -135,6 +146,9 @@ lane :bump do |options| paths_for_commit += sh("cd .. && git status --porcelain fastlane/swift | sed s/^...//").each_line.to_a.map(&:strip) paths_for_commit << "Gemfile.lock" + # Add CHANGELOG.latest + paths_for_commit << last_version_file_path + # Add version file path to change set paths_for_commit << version_file_path @@ -155,6 +169,7 @@ lane :bump do |options| pr_body << "**Changes since release '#{latest_version}':**" pr_body << changelog_text pr_url = create_pull_request( + api_bearer: github_api_bearer, api_token: github_api_token, repo: slug, title: commit_message, @@ -171,7 +186,9 @@ lane :bump do |options| end # Revert to master branch - sh("git checkout master") + unless is_automated_bump + sh("git checkout master") + end end desc "Generate the Swift api and test it" @@ -321,6 +338,101 @@ lane :release do slack_train end +desc "Creates a GitHub release with the gem as an asset" +lane :create_github_release do |options| + # Git verification + # + ensure_git_status_clean + ensure_git_branch(branch: 'master') + + # Verifying RubyGems version + # + version = local_version + old_version = current_version + if Gem::Version.new(version) <= Gem::Version.new(old_version) + UI.user_error!("Version number #{version} was already deployed") + end + + github_username = ENV["RELEASE_GITHUB_USERNAME"] || "fastlane" + repo_name = "#{github_username}/fastlane" + + # Preparing GitHub Release + # + github_release = get_github_release(url: repo_name, version: version, api_bearer: ENV['FASTLANE_RELEASE_API_BEARER']) + gem_path = if (github_release || {}).fetch('body', '').length == 0 + # Validate repo and create gem + validate_repo + gem_version_path = File.absolute_path("../pkg/fastlane-#{version}.gem") + + title = "Improvements" + description = File.read("../CHANGELOG.latest.md") + + github_release = set_github_release( + repository_name: repo_name, + name: [version, title].join(" "), + tag_name: version, + description: description, + is_draft: false, + upload_assets: [gem_version_path], + api_bearer: ENV['FASTLANE_RELEASE_API_BEARER'] + ) + + release_url = github_release['html_url'] + message = [title, description, release_url].join("\n\n") + add_fastlane_git_tag(tag: "fastlane/#{version}", message: message) + + UI.success("New GitHub release has been created: #{release_url}") + + gem_version_path + else + UI.message("GitHub release has already been created") + + sleep(50) # GitHub rate limits are annowing + download_github_release_gem(version: version) + end + + sh "gem push --key github --host https://rubygems.pkg.github.com/#{github_username} #{gem_path}" unless options[:skip_github_packages] + + sh "gem push #{gem_path}" unless options[:skip_rubygems] +end + +lane :publish_rubygems_from_github_release do |options| + +end + +lane :publish_github_packages_from_github_release do |options| + +end + +lane :download_github_release_gem do |options| + require 'open-uri' + + version = options[:version] + + github_username = ENV["RELEASE_GITHUB_USERNAME"] || "fastlane" + + github_release = get_github_release(url: "#{github_username}/fastlane", version: version, api_bearer: ENV['FASTLANE_RELEASE_API_BEARER']) + assets = (github_release || {}).fetch('assets', []) + + gem_asset = assets.find { |a| a["name"].end_with?(".gem") } + gem_asset_url = gem_asset["browser_download_url"] + + # Extract the file name from the URL + file_name = File.basename(URI.parse(gem_asset_url).path) + + FileUtils.mkdir_p('../pkg') + file_path = File.join('../pkg', file_name) + + # Download the file + URI.open(gem_asset_url) do |remote_file| + File.open(file_path, 'wb') do |local_file| + local_file.write(remote_file.read) + end + end + + file_path +end + lane :release_brew do version = local_version sh("cd .. && brew update && brew bump-formula-pr fastlane --force --url=https://github.com/fastlane/fastlane/archive/refs/tags/#{version}.tar.gz") @@ -353,11 +465,18 @@ private_lane :github_changelog do |options| body = JSON.parse(resp[:body]) commits = body["commits"].reverse + sleep_time = ENV['FL_CHANGELOG_SLEEP'].to_i + formatted = commits.map do |commit| + if sleep_time > 0 + UI.message("Sleeping for #{sleep_time} second(s) to get changelog") + sleep(sleep_time) + end + # Default to commit message info message = commit["commit"]["message"].lines.first.strip name = commit["commit"]["author"]["name"] - username = commit["author"]["login"] + username = commit["author"]&.[]("login") # Get pull request associate with commit message sha = commit["sha"] @@ -563,10 +682,12 @@ end desc "Ensure all the requirement environment variables are provided" desc "this way the deployment script will fail early (and often)" -private_lane :verify_env_variables do +private_lane :verify_env_variables do |options| ensure_env_vars(env_vars: ['GITHUB_USER_NAME', 'GITHUB_API_TOKEN']) - UI.user_error!("You're not logged in RubyGems. Log in using `gem push` if using RubyGems < 2.7.0 or `gem signin` if using RubyGems >=2.7.0") unless File.file?(File.expand_path("~/.gem/credentials")) + unless options[:skip_ruby_gems] + UI.user_error!("You're not logged in RubyGems. Log in using `gem push` if using RubyGems < 2.7.0 or `gem signin` if using RubyGems >=2.7.0") unless File.file?(File.expand_path("~/.gem/credentials")) + end end desc "Get the local version number per version.rb" @@ -590,6 +711,7 @@ private_lane :ensure_tool_name_formatting do errors = [] Dir.chdir("..") do Dir["**/*.md"].each do |path| + next if path == "CHANGELOG.latest.md" helper = Helper::ToolNameFormattingHelper.new(path: path, is_documenting_invalid_examples: path == 'CONTRIBUTING.md') errors += helper.find_tool_name_formatting_errors end diff --git a/fastlane/actions/plugin_scores.rb b/fastlane/actions/plugin_scores.rb index 2ede01ad4f9..de337ee01de 100644 --- a/fastlane/actions/plugin_scores.rb +++ b/fastlane/actions/plugin_scores.rb @@ -19,12 +19,13 @@ def self.run(params) end def self.fetch_plugins(cache_path) + require 'open-uri' page = 1 plugins = [] loop do url = "https://rubygems.org/api/v1/search.json?query=fastlane-plugin-&page=#{page}" puts("RubyGems API Request: #{url}") - results = JSON.parse(FastlaneCore::Helper.open_uri(url).read) + results = JSON.parse(URI.open(url).read) break if results.count == 0 plugins += results.collect do |current| diff --git a/fastlane/helper/plugin_scores_helper.rb b/fastlane/helper/plugin_scores_helper.rb index 3c9588348c3..756582d7665 100644 --- a/fastlane/helper/plugin_scores_helper.rb +++ b/fastlane/helper/plugin_scores_helper.rb @@ -52,7 +52,7 @@ def initialize(hash, cache_path) self.homepage = hash["homepage_uri"] || hash["documentation_uri"] self.raw_hash = hash - has_github_page = self.homepage.to_s.include?("https://github.com") # Here we can add non GitHub support one day + has_github_page = self.homepage.to_s.start_with?("https://github.com") # Here we can add non GitHub support one day self.data = { has_homepage: self.homepage.to_s.length > 5, diff --git a/fastlane/lib/fastlane/actions/app_store_build_number.rb b/fastlane/lib/fastlane/actions/app_store_build_number.rb index df01a3c415d..586215e7815 100644 --- a/fastlane/lib/fastlane/actions/app_store_build_number.rb +++ b/fastlane/lib/fastlane/actions/app_store_build_number.rb @@ -186,7 +186,7 @@ def self.available_options optional: true, default_value: "ios", verify_block: proc do |value| - UI.user_error!("The platform can only be ios, appletvos, or osx") unless %('ios', 'appletvos', 'osx').include?(value) + UI.user_error!("The platform can only be ios, appletvos, xros or osx") unless %('ios', 'appletvos', 'xros', 'osx').include?(value) end), FastlaneCore::ConfigItem.new(key: :team_name, short_option: "-e", diff --git a/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb b/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb index aaf0386ff7f..8e557f8cd86 100644 --- a/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb +++ b/fastlane/lib/fastlane/actions/app_store_connect_api_key.rb @@ -56,7 +56,8 @@ def self.available_options description: "The key ID"), FastlaneCore::ConfigItem.new(key: :issuer_id, env_name: "APP_STORE_CONNECT_API_KEY_ISSUER_ID", - description: "The issuer ID"), + description: "The issuer ID. It can be nil if the key is individual API key", + optional: true), FastlaneCore::ConfigItem.new(key: :key_filepath, env_name: "APP_STORE_CONNECT_API_KEY_KEY_FILEPATH", description: "The path to the key p8 file", diff --git a/fastlane/lib/fastlane/actions/appetize.rb b/fastlane/lib/fastlane/actions/appetize.rb index 58306c6ee6f..3d4235d3d51 100644 --- a/fastlane/lib/fastlane/actions/appetize.rb +++ b/fastlane/lib/fastlane/actions/appetize.rb @@ -50,6 +50,10 @@ def self.run(options) response = http.request(req) + unless response.code.to_i.between?(200, 299) + UI.user_error!("Error uploading to Appetize.io: received HTTP #{response.code} with body #{response.body}") + end + parse_response(response) # this will raise an exception if something goes wrong UI.message("App URL: #{Actions.lane_context[SharedValues::APPETIZE_APP_URL]}") diff --git a/fastlane/lib/fastlane/actions/changelog_from_git_commits.rb b/fastlane/lib/fastlane/actions/changelog_from_git_commits.rb index 6af9638312b..7a5d57e2e5a 100644 --- a/fastlane/lib/fastlane/actions/changelog_from_git_commits.rb +++ b/fastlane/lib/fastlane/actions/changelog_from_git_commits.rb @@ -37,9 +37,9 @@ def self.run(params) Dir.chdir(params[:path]) do if params[:commits_count] - changelog = Actions.git_log_last_commits(params[:pretty], params[:commits_count], merge_commit_filtering, params[:date_format], params[:ancestry_path]) + changelog = Actions.git_log_last_commits(params[:pretty], params[:commits_count], merge_commit_filtering, params[:date_format], params[:ancestry_path], params[:app_path]) else - changelog = Actions.git_log_between(params[:pretty], from, to, merge_commit_filtering, params[:date_format], params[:ancestry_path]) + changelog = Actions.git_log_between(params[:pretty], from, to, merge_commit_filtering, params[:date_format], params[:ancestry_path], params[:app_path]) end changelog = changelog.gsub("\n\n", "\n") if changelog # as there are duplicate newlines @@ -147,7 +147,11 @@ def self.available_options verify_block: proc do |value| matches_option = GIT_MERGE_COMMIT_FILTERING_OPTIONS.any? { |opt| opt.to_s == value } UI.user_error!("Valid values for :merge_commit_filtering are #{GIT_MERGE_COMMIT_FILTERING_OPTIONS.map { |o| "'#{o}'" }.join(', ')}") unless matches_option - end) + end), + FastlaneCore::ConfigItem.new(key: :app_path, + env_name: 'FL_CHANGELOG_FROM_GIT_COMMITS_APP_PATH', + description: "Scopes the changelog to a specific subdirectory of the repository", + optional: true) ] end diff --git a/fastlane/lib/fastlane/actions/docs/sync_code_signing.md b/fastlane/lib/fastlane/actions/docs/sync_code_signing.md index 6915d962a5a..9d6fd540012 100644 --- a/fastlane/lib/fastlane/actions/docs/sync_code_signing.md +++ b/fastlane/lib/fastlane/actions/docs/sync_code_signing.md @@ -509,12 +509,16 @@ Please be careful when using this option and ensure the certificates and profile ### Manual Decrypt -If you want to manually decrypt a file you can. +If you want to manually decrypt or encrypt a file, you can use the companion script `match_file`: ```no-highlight -openssl aes-256-cbc -k "" -in "" -out "" -a -d -md [md5|sha256] +match_file encrypt "" [""] + +match_file decrypt "" [""] ``` +The password will be asked interactively. + _**Note:** You may need to swap double quotes `"` for single quotes `'` if your match password contains an exclamation mark `!`._ #### Export Distribution Certificate and Private Key as Single .p12 File diff --git a/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb b/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb index 364206e90fa..ae8eb603d78 100644 --- a/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb +++ b/fastlane/lib/fastlane/actions/docs/upload_to_app_store.md.erb @@ -374,6 +374,7 @@ The available options: - 'ios' - 'appletvos' +- 'xros' - 'osx' @@ -431,12 +432,12 @@ end Omit `build_number` to let _fastlane_ automatically select the latest build number for the current version being edited for release from App Store Connect. -### Compliance and IDFA settings +### Compliance settings -Use the `submission_information` parameter for additional submission specifiers, including compliance and IDFA settings. Look at the Spaceship's [`app_submission.rb`](https://github.com/fastlane/fastlane/blob/master/spaceship/lib/spaceship/tunes/app_submission.rb) file for options. See [this example](https://github.com/artsy/eigen/blob/faa02e2746194d8d7c11899474de9c517435eca4/fastlane/Fastfile#L131-L149). +Use the `submission_information` parameter for additional submission specifiers, including compliance settings. Look at the Spaceship's [`app_submission.rb`](https://github.com/fastlane/fastlane/blob/master/spaceship/lib/spaceship/tunes/app_submission.rb) file for options. See [this example](https://github.com/artsy/eigen/blob/faa02e2746194d8d7c11899474de9c517435eca4/fastlane/Fastfile#L131-L149). ```no-highlight -fastlane deliver submit_build --build_number 830 --submission_information "{\"export_compliance_uses_encryption\": false, \"add_id_info_uses_idfa\": false }" +fastlane deliver submit_build --build_number 830 --submission_information "{\"export_compliance_uses_encryption\": false }" ``` ### App Privacy Details diff --git a/fastlane/lib/fastlane/actions/download_dsyms.rb b/fastlane/lib/fastlane/actions/download_dsyms.rb index a99ed645184..d096fa5614d 100644 --- a/fastlane/lib/fastlane/actions/download_dsyms.rb +++ b/fastlane/lib/fastlane/actions/download_dsyms.rb @@ -290,7 +290,7 @@ def self.available_options FastlaneCore::ConfigItem.new(key: :platform, short_option: "-p", env_name: "DOWNLOAD_DSYMS_PLATFORM", - description: "The app platform for dSYMs you wish to download (ios, appletvos)", + description: "The app platform for dSYMs you wish to download (ios, xros, appletvos)", default_value: :ios), FastlaneCore::ConfigItem.new(key: :version, short_option: "-v", @@ -351,7 +351,7 @@ def self.authors end def self.is_supported?(platform) - [:ios, :appletvos].include?(platform) + [:ios, :appletvos, :xros].include?(platform) end def self.example_code diff --git a/fastlane/lib/fastlane/actions/ensure_git_status_clean.rb b/fastlane/lib/fastlane/actions/ensure_git_status_clean.rb index 90373c1ce13..122562e7804 100644 --- a/fastlane/lib/fastlane/actions/ensure_git_status_clean.rb +++ b/fastlane/lib/fastlane/actions/ensure_git_status_clean.rb @@ -23,7 +23,9 @@ def self.run(params) # Manual post processing trying to ignore certain file paths if (ignore_files = params[:ignore_files]) repo_status = repo_status.lines.reject do |line| - path = line.split(' ').last + path = path_from_git_status_line(line) + next if path.empty? + was_found = ignore_files.include?(path) UI.message("Ignoring '#{path}'") if was_found @@ -54,6 +56,19 @@ def self.run(params) end end + def self.path_from_git_status_line(line) + # Extract the file path from the line based on https://git-scm.com/docs/git-status#_output. + # The first two characters indicate the status of the file path (e.g. ' M') + # M App/script.sh + # + # If the file path is renamed, the original path is also included in the line (e.g. 'R ORIG_PATH -> PATH') + # R App/script.sh -> App/script_renamed.sh + # + path = line.match(/^.. (.* -> )?(.*)$/)[2] + path = path.delete_prefix('"').delete_suffix('"') + return path + end + def self.description "Raises an exception if there are uncommitted git changes" end diff --git a/fastlane/lib/fastlane/actions/git_add.rb b/fastlane/lib/fastlane/actions/git_add.rb index d0c92d76c07..a5b27b572cd 100644 --- a/fastlane/lib/fastlane/actions/git_add.rb +++ b/fastlane/lib/fastlane/actions/git_add.rb @@ -17,7 +17,16 @@ def self.run(params) success_message = "Successfully added all files 💾." end - result = Actions.sh("git add #{paths}", log: FastlaneCore::Globals.verbose?).chomp + force = params[:force] ? "--force" : nil + + command = [ + "git", + "add", + force, + paths + ].compact + + result = Actions.sh(command.join(" "), log: FastlaneCore::Globals.verbose?).chomp UI.success(success_message) return result end @@ -47,6 +56,11 @@ def self.available_options type: Boolean, default_value: true, optional: true), + FastlaneCore::ConfigItem.new(key: :force, + description: "Allow adding otherwise ignored files", + type: Boolean, + default_value: false, + optional: true), # Deprecated FastlaneCore::ConfigItem.new(key: :pathspec, description: "The pathspec you want to add files from", @@ -76,7 +90,8 @@ def self.example_code 'git_add(path: "./Frameworks/*", shell_escape: false)', 'git_add(path: ["*.h", "*.m"], shell_escape: false)', 'git_add(path: "./Frameworks/*", shell_escape: false)', - 'git_add(path: "*.txt", shell_escape: false)' + 'git_add(path: "*.txt", shell_escape: false)', + 'git_add(path: "./tmp/.keep", force: true)' ] end diff --git a/fastlane/lib/fastlane/actions/latest_testflight_build_number.rb b/fastlane/lib/fastlane/actions/latest_testflight_build_number.rb index d3c44d299d5..84aacdfb1f3 100644 --- a/fastlane/lib/fastlane/actions/latest_testflight_build_number.rb +++ b/fastlane/lib/fastlane/actions/latest_testflight_build_number.rb @@ -83,7 +83,7 @@ def self.available_options optional: true, default_value: "ios", verify_block: proc do |value| - UI.user_error!("The platform can only be ios, osx, or appletvos") unless %('osx', ios', 'appletvos').include?(value) + UI.user_error!("The platform can only be ios, osx, xros or appletvos") unless %('osx', ios', 'appletvos', 'xros').include?(value) end), FastlaneCore::ConfigItem.new(key: :initial_build_number, env_name: "INITIAL_BUILD_NUMBER", diff --git a/fastlane/lib/fastlane/actions/mailgun.rb b/fastlane/lib/fastlane/actions/mailgun.rb index df70ba21b87..86bc2f93401 100644 --- a/fastlane/lib/fastlane/actions/mailgun.rb +++ b/fastlane/lib/fastlane/actions/mailgun.rb @@ -8,8 +8,15 @@ def self.is_supported?(platform) end def self.run(options) - Actions.verify_gem!('rest-client') - require 'rest-client' + Actions.verify_gem!('faraday') + Actions.verify_gem!('mime-types') + require 'faraday' + begin + # Use mime/types/columnar if available, for reduced memory usage + require 'mime/types/columnar' + rescue LoadError + require 'mime/types' + end handle_params_transition(options) mailgunit(options) end @@ -101,11 +108,15 @@ def self.author end def self.handle_params_transition(options) - options[:postmaster] = options[:mailgun_sandbox_postmaster] if options[:mailgun_sandbox_postmaster] - puts("\nUsing :mailgun_sandbox_postmaster is deprecated, please change to :postmaster".yellow) if options[:mailgun_sandbox_postmaster] + if options[:mailgun_sandbox_postmaster] && !options[:postmaster] + options[:postmaster] = options[:mailgun_sandbox_postmaster] + puts("\nUsing :mailgun_sandbox_postmaster is deprecated, please change to :postmaster".yellow) + end - options[:apikey] = options[:mailgun_apikey] if options[:mailgun_apikey] - puts("\nUsing :mailgun_apikey is deprecated, please change to :apikey".yellow) if options[:mailgun_apikey] + if options[:mailgun_apikey] && !options[:apikey] + options[:apikey] = options[:mailgun_apikey] + puts("\nUsing :mailgun_apikey is deprecated, please change to :apikey".yellow) + end end def self.mailgunit(options) @@ -122,14 +133,25 @@ def self.mailgunit(options) unless options[:attachment].nil? attachment_filenames = [*options[:attachment]] - attachments = attachment_filenames.map { |filename| File.new(filename, 'rb') } + attachments = attachment_filenames.map { |filename| Faraday::UploadIO.new(filename, mime_for(filename), filename) } params.store(:attachment, attachments) end - RestClient.post("https://api:#{options[:apikey]}@api.mailgun.net/v3/#{sandbox_domain}/messages", params) + conn = Faraday.new(url: "https://api:#{options[:apikey]}@api.mailgun.net") do |f| + f.request(:multipart) + f.request(:url_encoded) + f.adapter(:net_http) + end + response = conn.post("/v3/#{sandbox_domain}/messages", params) + UI.user_error!("Failed to send message via Mailgun, response: #{response.status}: #{response.body}.") if response.status != 200 mail_template(options) end + def self.mime_for(path) + mime = MIME::Types.type_for(path) + mime.empty? ? 'text/plain' : mime[0].content_type + end + def self.mail_template(options) hash = { author: Actions.git_author_email, diff --git a/fastlane/lib/fastlane/actions/onesignal.rb b/fastlane/lib/fastlane/actions/onesignal.rb index 80299808c49..638853d3e07 100644 --- a/fastlane/lib/fastlane/actions/onesignal.rb +++ b/fastlane/lib/fastlane/actions/onesignal.rb @@ -42,14 +42,19 @@ def self.run(params) payload["apns_p12_password"] = apns_p12_password || "" end + unless params[:fcm_json].nil? + data = File.read(params[:fcm_json]) + fcm_json = Base64.strict_encode64(data) + payload["fcm_v1_service_account_json"] = fcm_json + end + payload["gcm_key"] = android_token unless android_token.nil? payload["android_gcm_sender_id"] = android_gcm_sender_id unless android_gcm_sender_id.nil? payload["organization_id"] = organization_id unless organization_id.nil? # here's the actual lifting - POST or PUT to OneSignal - json_headers = { 'Content-Type' => 'application/json', 'Authorization' => "Basic #{auth_token}" } - url = +'https://onesignal.com/api/v1/apps' + url = +'https://api.onesignal.com/apps' url << '/' + app_id if is_update uri = URI.parse(url) http = Net::HTTP.new(uri.host, uri.port) @@ -122,6 +127,11 @@ def self.available_options sensitive: true, optional: true), + FastlaneCore::ConfigItem.new(key: :fcm_json, + env_name: "FCM_JSON", + description: "FCM Service Account JSON File (in .json format)", + optional: true), + FastlaneCore::ConfigItem.new(key: :apns_p12, env_name: "APNS_P12", description: "APNS P12 File (in .p12 format)", @@ -169,6 +179,7 @@ def self.example_code app_name: "Name for OneSignal App", android_token: "Your Android GCM key (optional)", android_gcm_sender_id: "Your Android GCM Sender ID (optional)", + fcm_json: "Path to FCM Service Account JSON File (optional)", apns_p12: "Path to Apple .p12 file (optional)", apns_p12_password: "Password for .p12 file (optional)", apns_env: "production/sandbox (defaults to production)", @@ -180,6 +191,7 @@ def self.example_code app_name: "New Name for OneSignal App", android_token: "Your Android GCM key (optional)", android_gcm_sender_id: "Your Android GCM Sender ID (optional)", + fcm_json: "Path to FCM Service Account JSON File (optional)", apns_p12: "Path to Apple .p12 file (optional)", apns_p12_password: "Password for .p12 file (optional)", apns_env: "production/sandbox (defaults to production)", diff --git a/fastlane/lib/fastlane/actions/set_changelog.rb b/fastlane/lib/fastlane/actions/set_changelog.rb index 1f503baafd7..39a8894911d 100644 --- a/fastlane/lib/fastlane/actions/set_changelog.rb +++ b/fastlane/lib/fastlane/actions/set_changelog.rb @@ -169,10 +169,10 @@ def self.available_options end), FastlaneCore::ConfigItem.new(key: :platform, env_name: "FL_SET_CHANGELOG_PLATFORM", - description: "The platform of the app (ios, appletvos, mac)", + description: "The platform of the app (ios, appletvos, xros, mac)", default_value: "ios", verify_block: proc do |value| - available = ['ios', 'appletvos', 'mac'] + available = ['ios', 'appletvos', 'xros', 'mac'] UI.user_error!("Invalid platform '#{value}', must be #{available.join(', ')}") unless available.include?(value) end) ] @@ -183,7 +183,7 @@ def self.authors end def self.is_supported?(platform) - [:ios, :appletvos, :mac].include?(platform) + [:ios, :appletvos, :xros, :mac].include?(platform) end def self.example_code diff --git a/fastlane/lib/fastlane/actions/slack.rb b/fastlane/lib/fastlane/actions/slack.rb index efc54a1e9be..0a87a6a3abb 100644 --- a/fastlane/lib/fastlane/actions/slack.rb +++ b/fastlane/lib/fastlane/actions/slack.rb @@ -27,6 +27,7 @@ def run(options) slack_attachment = self.class.generate_slack_attachments(options) link_names = options[:link_names] icon_url = options[:use_webhook_configured_username_and_icon] ? nil : options[:icon_url] + icon_emoji = options[:use_webhook_configured_username_and_icon] ? nil : options[:icon_emoji] post_message( channel: channel, @@ -34,16 +35,18 @@ def run(options) attachments: [slack_attachment], link_names: link_names, icon_url: icon_url, + icon_emoji: icon_emoji, fail_on_error: options[:fail_on_error] ) end - def post_message(channel:, username:, attachments:, link_names:, icon_url:, fail_on_error:) + def post_message(channel:, username:, attachments:, link_names:, icon_url:, icon_emoji:, fail_on_error:) @notifier.post_to_legacy_incoming_webhook( channel: channel, username: username, link_names: link_names, icon_url: icon_url, + icon_emoji: icon_emoji, attachments: attachments ) UI.success('Successfully sent Slack notification') @@ -211,9 +214,13 @@ def self.available_options optional: true), FastlaneCore::ConfigItem.new(key: :icon_url, env_name: "FL_SLACK_ICON_URL", - description: "Overrides the webhook's image property if use_webhook_configured_username_and_icon is false", + description: "Specifies a URL of an image to use as the photo of the message. Overrides the webhook's image property if use_webhook_configured_username_and_icon is false", default_value: "https://fastlane.tools/assets/img/fastlane_icon.png", optional: true), + FastlaneCore::ConfigItem.new(key: :icon_emoji, + env_name: "FL_SLACK_ICON_EMOJI", + description: "Specifies an emoji (using colon shortcodes, eg. :white_check_mark:) to use as the photo of the message. Overrides the webhook's image property if use_webhook_configured_username_and_icon is false. This parameter takes precedence over icon_url", + optional: true), FastlaneCore::ConfigItem.new(key: :payload, env_name: "FL_SLACK_PAYLOAD", description: "Add additional information to this post. payload must be a hash containing any key with any value", diff --git a/fastlane/lib/fastlane/actions/spm.rb b/fastlane/lib/fastlane/actions/spm.rb index ada882eecbe..482e26a8070 100644 --- a/fastlane/lib/fastlane/actions/spm.rb +++ b/fastlane/lib/fastlane/actions/spm.rb @@ -11,6 +11,7 @@ def self.run(params) cmd << "--configuration #{params[:configuration]}" if params[:configuration] cmd << "--disable-sandbox" if params[:disable_sandbox] cmd << "--verbose" if params[:verbose] + cmd << "--very-verbose" if params[:very_verbose] if params[:simulator] simulator_platform = simulator_platform(simulator: params[:simulator], simulator_arch: params[:simulator_arch]) simulator_sdk = simulator_sdk(simulator: params[:simulator]) @@ -118,6 +119,12 @@ def self.available_options description: "Increase verbosity of informational output", type: Boolean, default_value: false), + FastlaneCore::ConfigItem.new(key: :very_verbose, + short_option: "-V", + env_name: "FL_SPM_VERY_VERBOSE", + description: "Increase verbosity to include debug output", + type: Boolean, + default_value: false), FastlaneCore::ConfigItem.new(key: :simulator, env_name: "FL_SPM_SIMULATOR", description: "Specifies the simulator to pass for Swift Compiler (one of: #{valid_simulators.join(', ')})", diff --git a/fastlane/lib/fastlane/actions/testfairy.rb b/fastlane/lib/fastlane/actions/testfairy.rb index 2ee8703aff0..ddd45a128f4 100644 --- a/fastlane/lib/fastlane/actions/testfairy.rb +++ b/fastlane/lib/fastlane/actions/testfairy.rb @@ -96,6 +96,8 @@ def self.run(params) [key, options_to_client.call(value).join(',')] when :custom [key, value] + when :tags + [key, value.join(',')] else UI.user_error!("Unknown parameter: #{key}") end @@ -241,7 +243,13 @@ def self.available_options env_name: "FL_TESTFAIRY_TIMEOUT", description: "Request timeout in seconds", type: Integer, - optional: true) + optional: true), + FastlaneCore::ConfigItem.new(key: :tags, + optional: true, + env_name: "FL_TESTFAIRY_TAGS", + description: "Custom tags that can be used to organize your builds", + type: Array, + default_value: []) ] end diff --git a/fastlane/lib/fastlane/actions/update_project_provisioning.rb b/fastlane/lib/fastlane/actions/update_project_provisioning.rb index b68e6b36851..35c352b6881 100644 --- a/fastlane/lib/fastlane/actions/update_project_provisioning.rb +++ b/fastlane/lib/fastlane/actions/update_project_provisioning.rb @@ -21,8 +21,9 @@ def self.run(params) # download certificate unless File.exist?(params[:certificate]) && File.size(params[:certificate]) > 0 UI.message("Downloading root certificate from (#{ROOT_CERTIFICATE_URL}) to path '#{params[:certificate]}'") + require 'open-uri' File.open(params[:certificate], "w:ASCII-8BIT") do |file| - file.write(FastlaneCore::Helper.open_uri(ROOT_CERTIFICATE_URL, "rb").read) + file.write(URI.open(ROOT_CERTIFICATE_URL, "rb").read) end end diff --git a/fastlane/lib/fastlane/actions/upload_symbols_to_sentry.rb b/fastlane/lib/fastlane/actions/upload_symbols_to_sentry.rb index fcf33f9cb2f..0a6d8f55857 100644 --- a/fastlane/lib/fastlane/actions/upload_symbols_to_sentry.rb +++ b/fastlane/lib/fastlane/actions/upload_symbols_to_sentry.rb @@ -7,8 +7,10 @@ def self.run(params) UI.important("GitHub: https://github.com/getsentry/fastlane-plugin-sentry") UI.important("Installation: fastlane add_plugin sentry") - Actions.verify_gem!('rest-client') - require 'rest-client' + UI.user_error!("This plugin is now completely deprecated") + # the code below doesn't run anymore + # Actions.verify_gem!('rest-client') + # require 'rest-client' # Params - API host = params[:api_host] diff --git a/fastlane/lib/fastlane/commands_generator.rb b/fastlane/lib/fastlane/commands_generator.rb index be1e88da1af..6e507d40e24 100644 --- a/fastlane/lib/fastlane/commands_generator.rb +++ b/fastlane/lib/fastlane/commands_generator.rb @@ -249,6 +249,15 @@ def run end end + command :console do |c| + c.syntax = 'fastlane console' + c.description = 'Opens an interactive developer console' + c.action do |args, options| + require 'fastlane/console' + Fastlane::Console.execute(args, options) + end + end + command :enable_auto_complete do |c| c.syntax = 'fastlane enable_auto_complete' c.description = 'Enable tab auto completion' diff --git a/fastlane/lib/fastlane/console.rb b/fastlane/lib/fastlane/console.rb new file mode 100644 index 00000000000..48e6d96e1a9 --- /dev/null +++ b/fastlane/lib/fastlane/console.rb @@ -0,0 +1,24 @@ +require 'irb' + +module Fastlane + # Opens an interactive developer console + class Console + def self.execute(args, options) + ARGV.clear + IRB.setup(nil) + @irb = IRB::Irb.new(nil) + IRB.conf[:MAIN_CONTEXT] = @irb.context + IRB.conf[:PROMPT][:FASTLANE] = IRB.conf[:PROMPT][:SIMPLE].dup + IRB.conf[:PROMPT][:FASTLANE][:RETURN] = "%s\n" + @irb.context.prompt_mode = :FASTLANE + @irb.context.workspace = IRB::WorkSpace.new(binding) + trap('INT') do + @irb.signal_handle + end + + UI.message('Welcome to fastlane interactive!') + + catch(:IRB_EXIT) { @irb.eval_input } + end + end +end diff --git a/fastlane/lib/fastlane/fast_file.rb b/fastlane/lib/fastlane/fast_file.rb index 149c0619dc0..e804b4d837d 100644 --- a/fastlane/lib/fastlane/fast_file.rb +++ b/fastlane/lib/fastlane/fast_file.rb @@ -218,7 +218,9 @@ def sh(*args, &b) end def self.sh(*command, step_name: nil, log: true, error_callback: nil, &b) - command_header = log ? step_name || Actions.shell_command_from_args(*command) : "shell command" + command_header = step_name + command_header ||= log ? Actions.shell_command_from_args(*command) : "shell command" + Actions.execute_action(command_header) do Actions.sh_no_action(*command, log: log, error_callback: error_callback, &b) end @@ -343,7 +345,11 @@ def import_from_git(url: nil, branch: 'HEAD', path: 'fastlane/Fastfile', version # Update the repo if it's eligible for caching but the version isn't specified UI.message("Fetching remote git branches and updating git repo...") Helper.with_env_values('GIT_TERMINAL_PROMPT' => '0') do - Actions.sh("cd #{clone_folder.shellescape} && git fetch --all --quiet && git checkout #{checkout_param.shellescape} #{checkout_path} && git reset --hard && git rebase") + command = "cd #{clone_folder.shellescape} && git fetch --all --quiet && git checkout #{checkout_param.shellescape} #{checkout_path} && git reset --hard" + # Check if checked out "branch" is actually a branch or a tag + current_branch = Actions.sh("cd #{clone_folder.shellescape} && git rev-parse --abbrev-ref HEAD") + command << " && git rebase" unless current_branch.strip.eql?("HEAD") + Actions.sh(command) end else begin diff --git a/fastlane/lib/fastlane/helper/git_helper.rb b/fastlane/lib/fastlane/helper/git_helper.rb index 34892dc6fa0..a2cd6c26e51 100644 --- a/fastlane/lib/fastlane/helper/git_helper.rb +++ b/fastlane/lib/fastlane/helper/git_helper.rb @@ -9,13 +9,14 @@ module SharedValues end.freeze end - def self.git_log_between(pretty_format, from, to, merge_commit_filtering, date_format = nil, ancestry_path) + def self.git_log_between(pretty_format, from, to, merge_commit_filtering, date_format = nil, ancestry_path, app_path) command = %w(git log) command << "--pretty=#{pretty_format}" command << "--date=#{date_format}" if date_format command << '--ancestry-path' if ancestry_path command << "#{from}...#{to}" command << git_log_merge_commit_filtering_option(merge_commit_filtering) + command << app_path if app_path # "*command" syntax expands "command" array into variable arguments, which # will then be individually shell-escaped by Actions.sh. Actions.sh(*command.compact, log: false).chomp @@ -23,13 +24,14 @@ def self.git_log_between(pretty_format, from, to, merge_commit_filtering, date_f nil end - def self.git_log_last_commits(pretty_format, commit_count, merge_commit_filtering, date_format = nil, ancestry_path) + def self.git_log_last_commits(pretty_format, commit_count, merge_commit_filtering, date_format = nil, ancestry_path, app_path) command = %w(git log) command << "--pretty=#{pretty_format}" command << "--date=#{date_format}" if date_format command << '--ancestry-path' if ancestry_path command << '-n' << commit_count.to_s command << git_log_merge_commit_filtering_option(merge_commit_filtering) + command << app_path if app_path Actions.sh(*command.compact, log: false).chomp rescue nil diff --git a/fastlane/lib/fastlane/helper/sh_helper.rb b/fastlane/lib/fastlane/helper/sh_helper.rb index 722076a5517..35f307905dc 100644 --- a/fastlane/lib/fastlane/helper/sh_helper.rb +++ b/fastlane/lib/fastlane/helper/sh_helper.rb @@ -42,7 +42,7 @@ def self.sh_control_output(*command, print_command: true, print_command_output: result = '' exit_status = nil - if Helper.sh_enabled? + if FastlaneCore::Helper.sh_enabled? # The argument list is passed directly to Open3.popen2e, which # handles the variadic argument list in the same way as Kernel#spawn. # (http://ruby-doc.org/core-2.4.2/Kernel.html#method-i-spawn) or diff --git a/fastlane/lib/fastlane/lane_manager_base.rb b/fastlane/lib/fastlane/lane_manager_base.rb index 5fe6f9b7b89..fa1859f27e1 100644 --- a/fastlane/lib/fastlane/lane_manager_base.rb +++ b/fastlane/lib/fastlane/lane_manager_base.rb @@ -78,15 +78,23 @@ def self.print_lane_context end def self.print_error_line(ex) - error_line = ex.backtrace.first - return if error_line.nil? - - error_line = error_line.match("Fastfile:(\\d+):") - return unless error_line + lines = ex.backtrace_locations&.select { |loc| loc.path == 'Fastfile' }&.map(&:lineno) + return if lines.nil? || lines.empty? + + fastfile_content = File.read(FastlaneCore::FastlaneFolder.fastfile_path, encoding: "utf-8") + if ex.backtrace_locations.first&.path == 'Fastfile' + # If exception happened directly in the Fastfile itself (e.g. ArgumentError) + UI.error("Error in your Fastfile at line #{lines.first}") + UI.content_error(fastfile_content, lines.first) + lines.shift + end - line = error_line[1] - UI.error("Error in your Fastfile at line #{line}") - UI.content_error(File.read(FastlaneCore::FastlaneFolder.fastfile_path, encoding: "utf-8"), line) + unless lines.empty? + # If exception happened directly in the Fastfile, also print the caller (still from the Fastfile). + # If exception happened in _fastlane_ internal code, print the line from the Fastfile that called it + UI.error("Called from Fastfile at line #{lines.first}") + UI.content_error(fastfile_content, lines.first) + end end end end diff --git a/fastlane/lib/fastlane/notification/slack.rb b/fastlane/lib/fastlane/notification/slack.rb index b31058b6cce..5b7efee9505 100644 --- a/fastlane/lib/fastlane/notification/slack.rb +++ b/fastlane/lib/fastlane/notification/slack.rb @@ -8,16 +8,17 @@ def initialize(webhook_url) end end - # Overriding channel, icon_url and username is only supported in legacy incoming webhook. + # Overriding channel, icon_url, icon_emoji and username is only supported in legacy incoming webhook. # Also note that the use of attachments has been discouraged by Slack, in favor of Block Kit. # https://api.slack.com/legacy/custom-integrations/messaging/webhooks - def post_to_legacy_incoming_webhook(channel:, username:, attachments:, link_names:, icon_url:) + def post_to_legacy_incoming_webhook(channel:, username:, attachments:, link_names:, icon_url:, icon_emoji:) @client.post(@webhook_url) do |request| request.headers['Content-Type'] = 'application/json' request.body = { channel: channel, username: username, icon_url: icon_url, + icon_emoji: icon_emoji, attachments: attachments, link_names: link_names }.to_json diff --git a/fastlane/lib/fastlane/plugins/plugin_fetcher.rb b/fastlane/lib/fastlane/plugins/plugin_fetcher.rb index e1ac06fdaf8..68ec95d2649 100644 --- a/fastlane/lib/fastlane/plugins/plugin_fetcher.rb +++ b/fastlane/lib/fastlane/plugins/plugin_fetcher.rb @@ -7,13 +7,14 @@ class PluginFetcher # Returns an array of FastlanePlugin objects def self.fetch_gems(search_query: nil) require 'json' + require 'open-uri' page = 1 plugins = [] loop do url = "https://rubygems.org/api/v1/search.json?query=#{PluginManager.plugin_prefix}&page=#{page}" FastlaneCore::UI.verbose("RubyGems API Request: #{url}") - results = JSON.parse(FastlaneCore::Helper.open_uri(url).read) + results = JSON.parse(URI.open(url).read) break if results.count == 0 plugins += results.collect do |current| diff --git a/fastlane/lib/fastlane/plugins/plugin_info_collector.rb b/fastlane/lib/fastlane/plugins/plugin_info_collector.rb index 3c5f87b2fef..9a84acce588 100644 --- a/fastlane/lib/fastlane/plugins/plugin_info_collector.rb +++ b/fastlane/lib/fastlane/plugins/plugin_info_collector.rb @@ -66,8 +66,9 @@ def plugin_name_valid?(name) # Checks if the gem name is still free on RubyGems def gem_name_taken?(name) require 'json' + require 'open-uri' url = "https://rubygems.org/api/v1/gems/#{name}.json" - response = JSON.parse(FastlaneCore::Helper.open_uri(url).read) + response = JSON.parse(URI.open(url).read) return !!response['version'] rescue false diff --git a/fastlane/lib/fastlane/plugins/plugin_manager.rb b/fastlane/lib/fastlane/plugins/plugin_manager.rb index e2bca90ab6b..c0b791b4cf1 100644 --- a/fastlane/lib/fastlane/plugins/plugin_manager.rb +++ b/fastlane/lib/fastlane/plugins/plugin_manager.rb @@ -156,9 +156,10 @@ def attach_plugins_to_gemfile!(path_to_gemfile) def self.fetch_gem_info_from_rubygems(gem_name) require 'json' + require 'open-uri' url = "https://rubygems.org/api/v1/gems/#{gem_name}.json" begin - JSON.parse(FastlaneCore::Helper.open_uri(url).read) + JSON.parse(URI.open(url).read) rescue nil end diff --git a/fastlane/lib/fastlane/runner.rb b/fastlane/lib/fastlane/runner.rb index 88d755281b8..d711bbd9cef 100644 --- a/fastlane/lib/fastlane/runner.rb +++ b/fastlane/lib/fastlane/runner.rb @@ -40,13 +40,13 @@ def execute(lane, platform = nil, parameters = nil) return_val = nil path_to_use = FastlaneCore::FastlaneFolder.path || Dir.pwd - parameters ||= {} + parameters ||= {} # by default no parameters begin Dir.chdir(path_to_use) do # the file is located in the fastlane folder execute_flow_block(before_all_blocks, current_platform, current_lane, parameters) execute_flow_block(before_each_blocks, current_platform, current_lane, parameters) - return_val = lane_obj.call(parameters) # by default no parameters + return_val = lane_obj.call(parameters) # after blocks are only called if no exception was raised before # Call the platform specific after block and then the general one diff --git a/fastlane/lib/fastlane/version.rb b/fastlane/lib/fastlane/version.rb index 7d90b8fc92e..36cbe3b32d5 100644 --- a/fastlane/lib/fastlane/version.rb +++ b/fastlane/lib/fastlane/version.rb @@ -1,5 +1,6 @@ module Fastlane - VERSION = '2.219.0'.freeze + VERSION = '2.223.1'.freeze + SUMMARY = "The easiest way to build and release mobile apps.".freeze DESCRIPTION = "The easiest way to automate beta deployments and releases for your iOS and Android apps".freeze MINIMUM_XCODE_RELEASE = "7.0".freeze RUBOCOP_REQUIREMENT = '1.50.2'.freeze diff --git a/fastlane/spec/actions_specs/appetize_spec.rb b/fastlane/spec/actions_specs/appetize_spec.rb index 318644feaf8..d784a364630 100644 --- a/fastlane/spec/actions_specs/appetize_spec.rb +++ b/fastlane/spec/actions_specs/appetize_spec.rb @@ -34,6 +34,7 @@ allow(request).to receive(:body=).with(kind_of(String)).and_return(response) allow(response).to receive(:body).and_return(response_string) + allow(response).to receive(:code).and_return('200') end describe "Appetize Integration" do @@ -72,6 +73,19 @@ expect(Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::APPETIZE_API_HOST]).to eql('api.appetize.io') end + it "raises an error when the API returns an unsuccessful status code" do + allow(response).to receive(:code).and_return('401') + allow(response).to receive(:body).and_return('{"message": "Invalid Key"}') + expect do + Fastlane::FastFile.new.parse("lane :test do + appetize({ + api_token: '#{api_token}', + url: '#{url}' + }) + end").runner.execute(:test) + end.to raise_error(FastlaneCore::Interface::FastlaneError, /Error uploading to Appetize.io: received HTTP 401 with body {"message": "Invalid Key"}/) + end + it "works with custom API host" do expect do Fastlane::FastFile.new.parse("lane :test do diff --git a/fastlane/spec/actions_specs/changelog_from_git_commits_spec.rb b/fastlane/spec/actions_specs/changelog_from_git_commits_spec.rb index 659700f5338..3276607f3a4 100644 --- a/fastlane/spec/actions_specs/changelog_from_git_commits_spec.rb +++ b/fastlane/spec/actions_specs/changelog_from_git_commits_spec.rb @@ -190,6 +190,17 @@ expect(result).to eq(changelog) end + it "Returns a scoped log from the app's path if so requested" do + result = Fastlane::FastFile.new.parse("lane :test do + changelog_from_git_commits(app_path: './apps/ios') + end").runner.execute(:test) + + tag_name = %w(git rev-list --tags --max-count=1).shelljoin + describe = %W(git describe --tags #{tag_name}).shelljoin + changelog = %W(git log --pretty=%B #{describe}...HEAD ./apps/ios).shelljoin + expect(result).to eq(changelog) + end + it "Runs between option from command line" do options = FastlaneCore::Configuration.create(Fastlane::Actions::ChangelogFromGitCommitsAction.available_options, { diff --git a/fastlane/spec/actions_specs/ensure_git_status_clean_spec.rb b/fastlane/spec/actions_specs/ensure_git_status_clean_spec.rb index 5cb10c11562..b8563a9d74b 100644 --- a/fastlane/spec/actions_specs/ensure_git_status_clean_spec.rb +++ b/fastlane/spec/actions_specs/ensure_git_status_clean_spec.rb @@ -21,11 +21,11 @@ context "when git status is not clean" do before :each do - allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain", log: true).and_return("M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") - allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain", log: false).and_return("M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") - allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain --ignored='traditional'", log: true).and_return("M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb\n!! .DS_Store\n!! fastlane/") - allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain --ignored='no'", log: true).and_return("M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") - allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain --ignored='matching'", log: true).and_return("M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb\n!! .DS_Store\n!! fastlane/.DS_Store") + allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain", log: true).and_return(" M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") + allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain", log: false).and_return(" M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") + allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain --ignored='traditional'", log: true).and_return(" M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb\n!! .DS_Store\n!! fastlane/") + allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain --ignored='no'", log: true).and_return(" M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") + allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain --ignored='matching'", log: true).and_return(" M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb\n!! .DS_Store\n!! fastlane/.DS_Store") allow(Fastlane::Actions).to receive(:sh).with("git diff").and_return("+ \"this is a new line\"") end @@ -39,6 +39,28 @@ end").runner.execute(:test) end + it "outputs success message when a file path contains spaces" do + allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain", log: false).and_return(" M \"fastlane/spec/fixtures/git_commit/A FILE WITH SPACE\"") + + expect(FastlaneCore::UI).to receive(:success).with("Git status is clean, all good! 💪") + Fastlane::FastFile.new.parse("lane :test do + ensure_git_status_clean( + ignore_files: ['fastlane/spec/fixtures/git_commit/A FILE WITH SPACE'] + ) + end").runner.execute(:test) + end + + it "outputs success message when a file path is renamed" do + allow(Fastlane::Actions).to receive(:sh).with("git status --porcelain", log: false).and_return("R \"fastlane/spec/fixtures/git_commit/A FILE WITH SPACE\" -> \"fastlane/spec/fixtures/git_commit/A_FILE_WITHOUT_SPACE\"") + + expect(FastlaneCore::UI).to receive(:success).with("Git status is clean, all good! 💪") + Fastlane::FastFile.new.parse("lane :test do + ensure_git_status_clean( + ignore_files: ['fastlane/spec/fixtures/git_commit/A_FILE_WITHOUT_SPACE'] + ) + end").runner.execute(:test) + end + it "outputs rich error message" do expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.") Fastlane::FastFile.new.parse("lane :test do @@ -52,7 +74,7 @@ context "with show_uncommitted_changes flag" do context "true" do it "outputs rich error message" do - expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.\nUncommitted changes:\nM fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") + expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.\nUncommitted changes:\n M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") Fastlane::FastFile.new.parse("lane :test do ensure_git_status_clean(show_uncommitted_changes: true) end").runner.execute(:test) @@ -101,7 +123,7 @@ context "with ignored mode" do context "traditional" do it "outputs error message with ignored files" do - expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.\nUncommitted changes:\nM fastlane/lib/fastlane/actions/ensure_git_status_clean.rb\n!! .DS_Store\n!! fastlane/") + expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.\nUncommitted changes:\n M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb\n!! .DS_Store\n!! fastlane/") Fastlane::FastFile.new.parse("lane :test do ensure_git_status_clean(show_uncommitted_changes: true, ignored: 'traditional') end").runner.execute(:test) @@ -110,7 +132,7 @@ context "none" do it "outputs error message without ignored files" do - expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.\nUncommitted changes:\nM fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") + expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.\nUncommitted changes:\n M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") Fastlane::FastFile.new.parse("lane :test do ensure_git_status_clean(show_uncommitted_changes: true, ignored: 'none') end").runner.execute(:test) @@ -119,7 +141,7 @@ context "matching" do it "outputs error message with ignored files" do - expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.\nUncommitted changes:\nM fastlane/lib/fastlane/actions/ensure_git_status_clean.rb\n!! .DS_Store\n!! fastlane/.DS_Store") + expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.\nUncommitted changes:\n M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb\n!! .DS_Store\n!! fastlane/.DS_Store") Fastlane::FastFile.new.parse("lane :test do ensure_git_status_clean(show_uncommitted_changes: true, ignored: 'matching') end").runner.execute(:test) @@ -129,7 +151,7 @@ context "without ignored mode" do it "outputs error message without ignored files" do - expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.\nUncommitted changes:\nM fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") + expect(FastlaneCore::UI).to receive(:user_error!).with("Git repository is dirty! Please ensure the repo is in a clean state by committing/stashing/discarding all changes first.\nUncommitted changes:\n M fastlane/lib/fastlane/actions/ensure_git_status_clean.rb") Fastlane::FastFile.new.parse("lane :test do ensure_git_status_clean(show_uncommitted_changes: true) end").runner.execute(:test) diff --git a/fastlane/spec/actions_specs/flock_spec.rb b/fastlane/spec/actions_specs/flock_spec.rb index 195c6fc1d34..0bde55e351e 100644 --- a/fastlane/spec/actions_specs/flock_spec.rb +++ b/fastlane/spec/actions_specs/flock_spec.rb @@ -14,7 +14,7 @@ def run_flock(**arguments) context 'options' do before do ENV['FL_FLOCK_BASE_URL'] = 'https://example.com' - stub_request(:any, /example.com/) + stub_request(:any, /example\.com/) end it 'requires message' do diff --git a/fastlane/spec/actions_specs/git_add_spec.rb b/fastlane/spec/actions_specs/git_add_spec.rb index 67f59d3436e..bf50d4d534e 100644 --- a/fastlane/spec/actions_specs/git_add_spec.rb +++ b/fastlane/spec/actions_specs/git_add_spec.rb @@ -69,6 +69,17 @@ end end + context "as string with force option" do + let(:path) { "myfile.txt" } + + it "executes the correct git command" do + allow(Fastlane::Actions).to receive(:sh).with("git add --force #{path}", anything).and_return("") + result = Fastlane::FastFile.new.parse("lane :test do + git_add(path: '#{path}', force: true) + end").runner.execute(:test) + end + end + context "without parameters" do it "executes the correct git command" do allow(Fastlane::Actions).to receive(:sh).with("git add .", anything).and_return("") diff --git a/fastlane/spec/actions_specs/onesignal_spec.rb b/fastlane/spec/actions_specs/onesignal_spec.rb index a7564810ae3..7e7273df7b3 100644 --- a/fastlane/spec/actions_specs/onesignal_spec.rb +++ b/fastlane/spec/actions_specs/onesignal_spec.rb @@ -15,7 +15,7 @@ context 'and is create' do before :each do - stub_request(:post, 'https://onesignal.com/api/v1/apps').to_return(status: 200, body: '{}') + stub_request(:post, 'https://api.onesignal.com/apps').to_return(status: 200, body: '{}') end it 'outputs success message' do @@ -30,7 +30,7 @@ context 'and is update' do before :each do - stub_request(:put, "https://onesignal.com/api/v1/apps/#{app_id}").to_return(status: 200, body: '{}') + stub_request(:put, "https://api.onesignal.com/apps/#{app_id}").to_return(status: 200, body: '{}') end it 'outputs success message' do diff --git a/fastlane/spec/actions_specs/set_changelog_spec.rb b/fastlane/spec/actions_specs/set_changelog_spec.rb index 445ab7e2231..612db210b61 100644 --- a/fastlane/spec/actions_specs/set_changelog_spec.rb +++ b/fastlane/spec/actions_specs/set_changelog_spec.rb @@ -7,7 +7,7 @@ it 'raises a Fastlane error' do expect { Fastlane::FastFile.new.parse(invalidPlatform_lane).runner.execute(:test) }.to( raise_error(FastlaneCore::Interface::FastlaneError) do |error| - expect(error.message).to match(/Invalid platform 'whatever', must be ios, appletvos, mac/) + expect(error.message).to match(/Invalid platform 'whatever', must be ios, appletvos, xros, mac/) end ) end diff --git a/fastlane/spec/actions_specs/slack_spec.rb b/fastlane/spec/actions_specs/slack_spec.rb index c1811048500..6a6694b1cb5 100644 --- a/fastlane/spec/actions_specs/slack_spec.rb +++ b/fastlane/spec/actions_specs/slack_spec.rb @@ -22,6 +22,7 @@ message: message, success: false, channel: channel, + icon_emoji: ':white_check_mark:', payload: { 'Build Date' => Time.new.to_s, 'Built by' => 'Jenkins' @@ -46,6 +47,7 @@ ], link_names: false, icon_url: 'https://fastlane.tools/assets/img/fastlane_icon.png', + icon_emoji: ':white_check_mark:', fail_on_error: true } expect(subject).to receive(:post_message).with(expected_args) diff --git a/fastlane/spec/actions_specs/spm_spec.rb b/fastlane/spec/actions_specs/spm_spec.rb index 630855ef994..12c7428ca30 100644 --- a/fastlane/spec/actions_specs/spm_spec.rb +++ b/fastlane/spec/actions_specs/spm_spec.rb @@ -109,6 +109,22 @@ expect(result).to eq("swift build") end + it "adds very_verbose flag to command if very_verbose is set to true" do + result = Fastlane::FastFile.new.parse("lane :test do + spm(very_verbose: true) + end").runner.execute(:test) + + expect(result).to eq("swift build --very-verbose") + end + + it "doesn't add a very_verbose flag to command if very_verbose is set to false" do + result = Fastlane::FastFile.new.parse("lane :test do + spm(very_verbose: false) + end").runner.execute(:test) + + expect(result).to eq("swift build") + end + it "adds build-path flag to command if build_path is set" do result = Fastlane::FastFile.new.parse("lane :test do spm(build_path: 'foobar') @@ -255,6 +271,28 @@ expect(result).to eq("swift package #{command}") end + it "adds very_verbose flag to package command if very_verbose is set to true" do + result = Fastlane::FastFile.new.parse("lane :test do + spm( + command: '#{command}', + very_verbose: true + ) + end").runner.execute(:test) + + expect(result).to eq("swift package --very-verbose #{command}") + end + + it "doesn't add a very_verbose flag to package command if very_verbose is set to false" do + result = Fastlane::FastFile.new.parse("lane :test do + spm( + command: '#{command}', + very_verbose: false + ) + end").runner.execute(:test) + + expect(result).to eq("swift package #{command}") + end + it "adds build-path flag to package command if package_path is set to true" do result = Fastlane::FastFile.new.parse("lane :test do spm( @@ -380,6 +418,19 @@ expect(result).to eq("set -o pipefail && swift package --verbose generate-xcodeproj --xcconfig-overrides Package.xcconfig 2>&1 | xcpretty --simple") end + + it "adds --very-verbose and xcpretty options correctly as well" do + result = Fastlane::FastFile.new.parse("lane :test do + spm( + command: 'generate-xcodeproj', + xcconfig: 'Package.xcconfig', + very_verbose: true, + xcpretty_output: 'simple' + ) + end").runner.execute(:test) + + expect(result).to eq("set -o pipefail && swift package --very-verbose generate-xcodeproj --xcconfig-overrides Package.xcconfig 2>&1 | xcpretty --simple") + end end context "when simulator is specified" do diff --git a/fastlane/spec/actions_specs/testfairy_spec.rb b/fastlane/spec/actions_specs/testfairy_spec.rb index b1f24c569ce..754d5ac5a7d 100644 --- a/fastlane/spec/actions_specs/testfairy_spec.rb +++ b/fastlane/spec/actions_specs/testfairy_spec.rb @@ -111,7 +111,8 @@ upload_url: 'https://your-subdomain.testfairy.com', comment: 'Test Comment!', testers_groups: ['group1', 'group2'], - custom: 'custom argument' + custom: 'custom argument', + tags: ['tag1', 'tag2', 'tag3'] }) end").runner.execute(:test) end.not_to(raise_error) diff --git a/fastlane/spec/actions_specs/upload_symbols_to_sentry_spec.rb b/fastlane/spec/actions_specs/upload_symbols_to_sentry_spec.rb deleted file mode 100644 index a84cadf2c1f..00000000000 --- a/fastlane/spec/actions_specs/upload_symbols_to_sentry_spec.rb +++ /dev/null @@ -1,83 +0,0 @@ -describe Fastlane do - describe Fastlane::FastFile do - describe "sentry" do - before do - # Prevent ENV vars that might be defined in the developer's machine to muddy the test environment - allow(ENV).to receive(:[]).and_return(nil) - end - - it "fails with no API key or auth token" do - dsym_path_1 = './fastlane/spec/fixtures/dSYM/Themoji.dSYM.zip' - - expect do - result = Fastlane::FastFile.new.parse("lane :test do - upload_symbols_to_sentry( - org_slug: 'some_org', - project_slug: 'some_project', - dsym_path: '#{dsym_path_1}') - end").runner.execute(:test) - end.to raise_error("No API key or authentication token found for SentryAction given, pass using `api_key: 'key'` or `auth_token: 'token'`") - end - - it "fails with API key and auth token" do - dsym_path_1 = './fastlane/spec/fixtures/dSYM/Themoji.dSYM.zip' - - expect do - result = Fastlane::FastFile.new.parse("lane :test do - upload_symbols_to_sentry( - org_slug: 'some_org', - api_key: 'something123', - auth_token: 'something123', - project_slug: 'some_project', - dsym_path: '#{dsym_path_1}') - end").runner.execute(:test) - end.to raise_error("Both API key and authentication token found for SentryAction given, please only give one") - end - - it "returns uploaded dSYM path using API key" do - dsym_path_1 = './fastlane/spec/fixtures/dSYM/Themoji.dSYM.zip' - - result = Fastlane::FastFile.new.parse("lane :test do - upload_symbols_to_sentry( - api_key: 'something123', - org_slug: 'some_org', - project_slug: 'some_project', - dsym_path: '#{dsym_path_1}') - end").runner.execute(:test) - - expect(result).to include(dsym_path_1) - end - - it "returns uploaded dSYM path using auth token" do - dsym_path_1 = './fastlane/spec/fixtures/dSYM/Themoji.dSYM.zip' - - result = Fastlane::FastFile.new.parse("lane :test do - upload_symbols_to_sentry( - auth_token: 'something123', - org_slug: 'some_org', - project_slug: 'some_project', - dsym_path: '#{dsym_path_1}') - end").runner.execute(:test) - - expect(result).to include(dsym_path_1) - end - - it "returns uploaded dSYM paths" do - dsym_path_1 = './fastlane/spec/fixtures/dSYM/Themoji.dSYM.zip' - dsym_path_2 = './fastlane/spec/fixtures/dSYM/This_doesnt_exist_but_doesnt_need_to.dSYM.zip' - - result = Fastlane::FastFile.new.parse("lane :test do - upload_symbols_to_sentry( - api_key: 'something123', - org_slug: 'some_org', - project_slug: 'some_project', - dsym_path: '#{dsym_path_1}', - dsym_paths: ['#{dsym_path_2}']) - end").runner.execute(:test) - - expect(result).to include(dsym_path_1) - expect(result).to include(dsym_path_2) - end - end - end -end diff --git a/fastlane/spec/env_spec.rb b/fastlane/spec/env_spec.rb index 1525b19e482..274ed8cd8fa 100644 --- a/fastlane/spec/env_spec.rb +++ b/fastlane/spec/env_spec.rb @@ -4,7 +4,7 @@ describe Fastlane do describe Fastlane::EnvironmentPrinter do before do - stub_request(:get, %r{https:\/\/rubygems.org\/api\/v1\/gems\/.*}). + stub_request(:get, %r{https://rubygems\.org\/api\/v1\/gems\/.*}). to_return(status: 200, body: '{"version": "0.16.2"}', headers: {}) end diff --git a/fastlane/spec/fast_file_spec.rb b/fastlane/spec/fast_file_spec.rb index bb1504c4080..f3cb0cd2b95 100644 --- a/fastlane/spec/fast_file_spec.rb +++ b/fastlane/spec/fast_file_spec.rb @@ -49,7 +49,7 @@ end it "passes command as string, step_name, and log false with default error_callback" do - expect(Fastlane::Actions).to receive(:execute_action).with("shell command").and_call_original + expect(Fastlane::Actions).to receive(:execute_action).with("some_name").and_call_original expect(Fastlane::Actions).to receive(:sh_no_action) .with("git commit", log: false, error_callback: nil) diff --git a/fastlane/spec/fastlane_require_spec.rb b/fastlane/spec/fastlane_require_spec.rb index e7c5269e3dd..1c94e58cf65 100644 --- a/fastlane/spec/fastlane_require_spec.rb +++ b/fastlane/spec/fastlane_require_spec.rb @@ -9,9 +9,9 @@ end it "formats gem require name for non-fastlane-plugin" do - gem_name = "rest-client" + gem_name = "some-lib" gem_require_name = Fastlane::FastlaneRequire.format_gem_require_name(gem_name) - expect(gem_require_name).to eq("rest-client") + expect(gem_require_name).to eq("some-lib") end describe "checks if a gem is installed" do diff --git a/fastlane/spec/lane_manager_base_spec.rb b/fastlane/spec/lane_manager_base_spec.rb new file mode 100644 index 00000000000..fb85776780a --- /dev/null +++ b/fastlane/spec/lane_manager_base_spec.rb @@ -0,0 +1,36 @@ +describe Fastlane do + describe Fastlane::LaneManagerBase do + describe "#print_lane_context" do + it "prints lane context" do + Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::LANE_NAME] = "test" + + cleaned_row_data = [[:LANE_NAME, "test"]] + + table_data = FastlaneCore::PrintTable.transform_output(cleaned_row_data) + expect(FastlaneCore::PrintTable).to receive(:transform_output).with(cleaned_row_data).and_call_original + expect(Terminal::Table).to receive(:new).with({ + title: "Lane Context".yellow, + rows: table_data + }) + Fastlane::LaneManagerBase.print_lane_context + end + + it "prints lane context" do + Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::LANE_NAME] = "test" + + expect(UI).to receive(:important).with('Lane Context:'.yellow) + expect(UI).to receive(:message).with(Fastlane::Actions.lane_context) + + FastlaneSpec::Env.with_verbose(true) do + Fastlane::LaneManagerBase.print_lane_context + end + end + + it "doesn't crash when lane_context contains non unicode text" do + Fastlane::Actions.lane_context[Fastlane::Actions::SharedValues::LANE_NAME] = "test\xAE" + + Fastlane::LaneManagerBase.print_lane_context + end + end + end +end diff --git a/fastlane/swift/Deliverfile.swift b/fastlane/swift/Deliverfile.swift index b6e6608e41a..3a4ad6e7ffe 100644 --- a/fastlane/swift/Deliverfile.swift +++ b/fastlane/swift/Deliverfile.swift @@ -17,4 +17,4 @@ public class Deliverfile: DeliverfileProtocol { // during the `init` process, and you won't see this message } -// Generated with fastlane 2.219.0 +// Generated with fastlane 2.223.1 diff --git a/fastlane/swift/DeliverfileProtocol.swift b/fastlane/swift/DeliverfileProtocol.swift index f778bb2fbdb..4f31362eab4 100644 --- a/fastlane/swift/DeliverfileProtocol.swift +++ b/fastlane/swift/DeliverfileProtocol.swift @@ -59,6 +59,9 @@ public protocol DeliverfileProtocol: AnyObject { /// Clear all previously uploaded screenshots before uploading the new ones var overwriteScreenshots: Bool { get } + /// Timeout in seconds to wait before considering screenshot processing as failed, used to handle cases where uploads to the App Store are stuck in processing + var screenshotProcessingTimeout: Int { get } + /// Sync screenshots with local ones. This is currently beta option so set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well var syncScreenshots: Bool { get } @@ -71,6 +74,9 @@ public protocol DeliverfileProtocol: AnyObject { /// Rejects the previously submitted build if it's in a state where it's possible var rejectIfPossible: Bool { get } + /// After submitting a new version, App Store Connect takes some time to recognize the new version and we must wait until it's available before attempting to upload metadata for it. There is a mechanism that will check if it's available and retry with an exponential backoff if it's not available yet. This option specifies how many times we should retry before giving up. Setting this to a value below 5 is not recommended and will likely cause failures. Increase this parameter when Apple servers seem to be degraded or slow + var versionCheckWaitRetryLimit: Int { get } + /// Should the app be automatically released once it's approved? (Cannot be used together with `auto_release_date`) var automaticRelease: Bool? { get } @@ -89,7 +95,7 @@ public protocol DeliverfileProtocol: AnyObject { /// Path to the app rating's config var appRatingConfigPath: String? { get } - /// Extra information for the submission (e.g. compliance specifications, IDFA settings) + /// Extra information for the submission (e.g. compliance specifications) var submissionInformation: [String: Any]? { get } /// The ID of your App Store Connect team if you're in multiple teams @@ -215,10 +221,12 @@ public extension DeliverfileProtocol { var skipAppVersionUpdate: Bool { return false } var force: Bool { return false } var overwriteScreenshots: Bool { return false } + var screenshotProcessingTimeout: Int { return 3600 } var syncScreenshots: Bool { return false } var submitForReview: Bool { return false } var verifyOnly: Bool { return false } var rejectIfPossible: Bool { return false } + var versionCheckWaitRetryLimit: Int { return 7 } var automaticRelease: Bool? { return nil } var autoReleaseDate: Int? { return nil } var phasedRelease: Bool { return false } @@ -264,4 +272,4 @@ public extension DeliverfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.123] +// FastlaneRunnerAPIVersion [0.9.129] diff --git a/fastlane/swift/Fastlane.swift b/fastlane/swift/Fastlane.swift index b80127d8d40..bc2b910da6b 100644 --- a/fastlane/swift/Fastlane.swift +++ b/fastlane/swift/Fastlane.swift @@ -2,6 +2,7 @@ // Copyright (c) 2024 FastlaneTools import Foundation + /** Run ADB Actions @@ -189,7 +190,7 @@ public func appStoreBuildNumber(apiKeyPath: OptionalConfigValue = .fast - parameters: - keyId: The key ID - - issuerId: The issuer ID + - issuerId: The issuer ID. It can be nil if the key is individual API key - keyFilepath: The path to the key p8 file - keyContent: The content of the key p8 file - isKeyContentBase64: Whether :key_content is Base64 encoded or not @@ -200,7 +201,7 @@ public func appStoreBuildNumber(apiKeyPath: OptionalConfigValue = .fast Load the App Store Connect API token to use in other fastlane tools and actions */ public func appStoreConnectApiKey(keyId: String, - issuerId: String, + issuerId: OptionalConfigValue = .fastlaneDefault(nil), keyFilepath: OptionalConfigValue = .fastlaneDefault(nil), keyContent: OptionalConfigValue = .fastlaneDefault(nil), isKeyContentBase64: OptionalConfigValue = .fastlaneDefault(false), @@ -209,7 +210,7 @@ public func appStoreConnectApiKey(keyId: String, setSpaceshipToken: OptionalConfigValue = .fastlaneDefault(true)) { let keyIdArg = RubyCommand.Argument(name: "key_id", value: keyId, type: nil) - let issuerIdArg = RubyCommand.Argument(name: "issuer_id", value: issuerId, type: nil) + let issuerIdArg = issuerId.asRubyArgument(name: "issuer_id", type: nil) let keyFilepathArg = keyFilepath.asRubyArgument(name: "key_filepath", type: nil) let keyContentArg = keyContent.asRubyArgument(name: "key_content", type: nil) let isKeyContentBase64Arg = isKeyContentBase64.asRubyArgument(name: "is_key_content_base64", type: nil) @@ -661,17 +662,19 @@ public func appledoc(input: [String], - skipAppVersionUpdate: Don’t create or update the app version that is being prepared for submission - force: Skip verification of HTML preview file - overwriteScreenshots: Clear all previously uploaded screenshots before uploading the new ones + - screenshotProcessingTimeout: Timeout in seconds to wait before considering screenshot processing as failed, used to handle cases where uploads to the App Store are stuck in processing - syncScreenshots: Sync screenshots with local ones. This is currently beta option so set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well - submitForReview: Submit the new version for Review after uploading everything - verifyOnly: Verifies archive with App Store Connect without uploading - rejectIfPossible: Rejects the previously submitted build if it's in a state where it's possible + - versionCheckWaitRetryLimit: After submitting a new version, App Store Connect takes some time to recognize the new version and we must wait until it's available before attempting to upload metadata for it. There is a mechanism that will check if it's available and retry with an exponential backoff if it's not available yet. This option specifies how many times we should retry before giving up. Setting this to a value below 5 is not recommended and will likely cause failures. Increase this parameter when Apple servers seem to be degraded or slow - automaticRelease: Should the app be automatically released once it's approved? (Cannot be used together with `auto_release_date`) - autoReleaseDate: Date in milliseconds for automatically releasing on pending approval (Cannot be used together with `automatic_release`) - phasedRelease: Enable the phased release feature of iTC - resetRatings: Reset the summary rating when you release a new version of the application - priceTier: The price tier of this application - appRatingConfigPath: Path to the app rating's config - - submissionInformation: Extra information for the submission (e.g. compliance specifications, IDFA settings) + - submissionInformation: Extra information for the submission (e.g. compliance specifications) - teamId: The ID of your App Store Connect team if you're in multiple teams - teamName: The name of your App Store Connect team if you're in multiple teams - devPortalTeamId: The short ID of your Developer Portal team, if you're in multiple teams. Different from your iTC team ID! @@ -733,10 +736,12 @@ public func appstore(apiKeyPath: OptionalConfigValue = .fastlaneDefault skipAppVersionUpdate: OptionalConfigValue = .fastlaneDefault(false), force: OptionalConfigValue = .fastlaneDefault(false), overwriteScreenshots: OptionalConfigValue = .fastlaneDefault(false), + screenshotProcessingTimeout: Int = 3600, syncScreenshots: OptionalConfigValue = .fastlaneDefault(false), submitForReview: OptionalConfigValue = .fastlaneDefault(false), verifyOnly: OptionalConfigValue = .fastlaneDefault(false), rejectIfPossible: OptionalConfigValue = .fastlaneDefault(false), + versionCheckWaitRetryLimit: Int = 7, automaticRelease: OptionalConfigValue = .fastlaneDefault(nil), autoReleaseDate: OptionalConfigValue = .fastlaneDefault(nil), phasedRelease: OptionalConfigValue = .fastlaneDefault(false), @@ -798,10 +803,12 @@ public func appstore(apiKeyPath: OptionalConfigValue = .fastlaneDefault let skipAppVersionUpdateArg = skipAppVersionUpdate.asRubyArgument(name: "skip_app_version_update", type: nil) let forceArg = force.asRubyArgument(name: "force", type: nil) let overwriteScreenshotsArg = overwriteScreenshots.asRubyArgument(name: "overwrite_screenshots", type: nil) + let screenshotProcessingTimeoutArg = RubyCommand.Argument(name: "screenshot_processing_timeout", value: screenshotProcessingTimeout, type: nil) let syncScreenshotsArg = syncScreenshots.asRubyArgument(name: "sync_screenshots", type: nil) let submitForReviewArg = submitForReview.asRubyArgument(name: "submit_for_review", type: nil) let verifyOnlyArg = verifyOnly.asRubyArgument(name: "verify_only", type: nil) let rejectIfPossibleArg = rejectIfPossible.asRubyArgument(name: "reject_if_possible", type: nil) + let versionCheckWaitRetryLimitArg = RubyCommand.Argument(name: "version_check_wait_retry_limit", value: versionCheckWaitRetryLimit, type: nil) let automaticReleaseArg = automaticRelease.asRubyArgument(name: "automatic_release", type: nil) let autoReleaseDateArg = autoReleaseDate.asRubyArgument(name: "auto_release_date", type: nil) let phasedReleaseArg = phasedRelease.asRubyArgument(name: "phased_release", type: nil) @@ -862,10 +869,12 @@ public func appstore(apiKeyPath: OptionalConfigValue = .fastlaneDefault skipAppVersionUpdateArg, forceArg, overwriteScreenshotsArg, + screenshotProcessingTimeoutArg, syncScreenshotsArg, submitForReviewArg, verifyOnlyArg, rejectIfPossibleArg, + versionCheckWaitRetryLimitArg, automaticReleaseArg, autoReleaseDateArg, phasedReleaseArg, @@ -1333,6 +1342,7 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file - useSystemScm: Lets xcodebuild use system's scm configuration + - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - returns: The absolute path to the generated ipa file @@ -1387,7 +1397,8 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), - useSystemScm: OptionalConfigValue = .fastlaneDefault(false)) -> String + useSystemScm: OptionalConfigValue = .fastlaneDefault(false), + packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil)) -> String { let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) let projectArg = project.asRubyArgument(name: "project", type: nil) @@ -1439,6 +1450,7 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) + let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) let array: [RubyCommand.Argument?] = [workspaceArg, projectArg, schemeArg, @@ -1488,7 +1500,8 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul clonedSourcePackagesPathArg, skipPackageDependenciesResolutionArg, disablePackageAutomaticUpdatesArg, - useSystemScmArg] + useSystemScmArg, + packageAuthorizationProviderArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } .compactMap { $0 } @@ -1547,6 +1560,7 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file - useSystemScm: Lets xcodebuild use system's scm configuration + - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - returns: The absolute path to the generated ipa file @@ -1598,7 +1612,8 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), - useSystemScm: OptionalConfigValue = .fastlaneDefault(false)) -> String + useSystemScm: OptionalConfigValue = .fastlaneDefault(false), + packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil)) -> String { let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) let projectArg = project.asRubyArgument(name: "project", type: nil) @@ -1647,6 +1662,7 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) + let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) let array: [RubyCommand.Argument?] = [workspaceArg, projectArg, schemeArg, @@ -1693,7 +1709,8 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul clonedSourcePackagesPathArg, skipPackageDependenciesResolutionArg, disablePackageAutomaticUpdatesArg, - useSystemScmArg] + useSystemScmArg, + packageAuthorizationProviderArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } .compactMap { $0 } @@ -1753,6 +1770,7 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file - useSystemScm: Lets xcodebuild use system's scm configuration + - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - returns: The absolute path to the generated ipa file @@ -1805,7 +1823,8 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), - useSystemScm: OptionalConfigValue = .fastlaneDefault(false)) -> String + useSystemScm: OptionalConfigValue = .fastlaneDefault(false), + packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil)) -> String { let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) let projectArg = project.asRubyArgument(name: "project", type: nil) @@ -1855,6 +1874,7 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) + let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) let array: [RubyCommand.Argument?] = [workspaceArg, projectArg, schemeArg, @@ -1902,7 +1922,8 @@ public func buildAndroidApp(task: OptionalConfigValue = .fastlaneDefaul clonedSourcePackagesPathArg, skipPackageDependenciesResolutionArg, disablePackageAutomaticUpdatesArg, - useSystemScmArg] + useSystemScmArg, + packageAuthorizationProviderArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } .compactMap { $0 } @@ -2108,8 +2129,8 @@ public func captureAndroidScreenshots(androidHome: OptionalConfigValue Generate new localized screenshots on multiple devices (via _snapshot_) - parameters: - - workspace: Path the workspace file - - project: Path the project file + - workspace: Path to the workspace file + - project: Path to the project file - xcargs: Pass additional arguments to xcodebuild for the test phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - xcconfig: Use an extra XCCONFIG file to build your app - devices: A list of devices you want to take the screenshots from @@ -2149,6 +2170,7 @@ public func captureAndroidScreenshots(androidHome: OptionalConfigValue - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file + - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - testplan: The testplan associated with the scheme that should be used for testing - onlyTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to run - skipTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to skip @@ -2199,6 +2221,7 @@ public func captureIosScreenshots(workspace: OptionalConfigValue = .fas clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), + packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil), testplan: OptionalConfigValue = .fastlaneDefault(nil), onlyTesting: Any? = nil, skipTesting: Any? = nil, @@ -2249,6 +2272,7 @@ public func captureIosScreenshots(workspace: OptionalConfigValue = .fas let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) + let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) let testplanArg = testplan.asRubyArgument(name: "testplan", type: nil) let onlyTestingArg = RubyCommand.Argument(name: "only_testing", value: onlyTesting, type: nil) let skipTestingArg = RubyCommand.Argument(name: "skip_testing", value: skipTesting, type: nil) @@ -2298,6 +2322,7 @@ public func captureIosScreenshots(workspace: OptionalConfigValue = .fas clonedSourcePackagesPathArg, skipPackageDependenciesResolutionArg, disablePackageAutomaticUpdatesArg, + packageAuthorizationProviderArg, testplanArg, onlyTestingArg, skipTestingArg, @@ -2317,8 +2342,8 @@ public func captureIosScreenshots(workspace: OptionalConfigValue = .fas Alias for the `capture_ios_screenshots` action - parameters: - - workspace: Path the workspace file - - project: Path the project file + - workspace: Path to the workspace file + - project: Path to the project file - xcargs: Pass additional arguments to xcodebuild for the test phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - xcconfig: Use an extra XCCONFIG file to build your app - devices: A list of devices you want to take the screenshots from @@ -2358,6 +2383,7 @@ public func captureIosScreenshots(workspace: OptionalConfigValue = .fas - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file + - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - testplan: The testplan associated with the scheme that should be used for testing - onlyTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to run - skipTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to skip @@ -2408,6 +2434,7 @@ public func captureScreenshots(workspace: OptionalConfigValue = .fastla clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(nil), skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(false), disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), + packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil), testplan: OptionalConfigValue = .fastlaneDefault(nil), onlyTesting: Any? = nil, skipTesting: Any? = nil, @@ -2458,6 +2485,7 @@ public func captureScreenshots(workspace: OptionalConfigValue = .fastla let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) + let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) let testplanArg = testplan.asRubyArgument(name: "testplan", type: nil) let onlyTestingArg = RubyCommand.Argument(name: "only_testing", value: onlyTesting, type: nil) let skipTestingArg = RubyCommand.Argument(name: "skip_testing", value: skipTesting, type: nil) @@ -2507,6 +2535,7 @@ public func captureScreenshots(workspace: OptionalConfigValue = .fastla clonedSourcePackagesPathArg, skipPackageDependenciesResolutionArg, disablePackageAutomaticUpdatesArg, + packageAuthorizationProviderArg, testplanArg, onlyTestingArg, skipTestingArg, @@ -2718,6 +2747,7 @@ public func cert(development: OptionalConfigValue = .fastlaneDefault(false - quiet: Whether or not to disable changelog output - includeMerges: **DEPRECATED!** Use `:merge_commit_filtering` instead - Whether or not to include any commits that are merges - mergeCommitFiltering: Controls inclusion of merge commits when collecting the changelog. Valid values: 'include_merges', 'exclude_merges', 'only_include_merges' + - appPath: Scopes the changelog to a specific subdirectory of the repository - returns: Returns a String containing your formatted git commits @@ -2733,7 +2763,8 @@ public func cert(development: OptionalConfigValue = .fastlaneDefault(false matchLightweightTag: OptionalConfigValue = .fastlaneDefault(true), quiet: OptionalConfigValue = .fastlaneDefault(false), includeMerges: OptionalConfigValue = .fastlaneDefault(nil), - mergeCommitFiltering: String = "include_merges") -> String + mergeCommitFiltering: String = "include_merges", + appPath: OptionalConfigValue = .fastlaneDefault(nil)) -> String { let betweenArg = between.asRubyArgument(name: "between", type: nil) let commitsCountArg = commitsCount.asRubyArgument(name: "commits_count", type: nil) @@ -2746,6 +2777,7 @@ public func cert(development: OptionalConfigValue = .fastlaneDefault(false let quietArg = quiet.asRubyArgument(name: "quiet", type: nil) let includeMergesArg = includeMerges.asRubyArgument(name: "include_merges", type: nil) let mergeCommitFilteringArg = RubyCommand.Argument(name: "merge_commit_filtering", value: mergeCommitFiltering, type: nil) + let appPathArg = appPath.asRubyArgument(name: "app_path", type: nil) let array: [RubyCommand.Argument?] = [betweenArg, commitsCountArg, pathArg, @@ -2756,7 +2788,8 @@ public func cert(development: OptionalConfigValue = .fastlaneDefault(false matchLightweightTagArg, quietArg, includeMergesArg, - mergeCommitFilteringArg] + mergeCommitFilteringArg, + appPathArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } .compactMap { $0 } @@ -3693,17 +3726,19 @@ public func deleteKeychain(name: OptionalConfigValue = .fastlaneDefault - skipAppVersionUpdate: Don’t create or update the app version that is being prepared for submission - force: Skip verification of HTML preview file - overwriteScreenshots: Clear all previously uploaded screenshots before uploading the new ones + - screenshotProcessingTimeout: Timeout in seconds to wait before considering screenshot processing as failed, used to handle cases where uploads to the App Store are stuck in processing - syncScreenshots: Sync screenshots with local ones. This is currently beta option so set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well - submitForReview: Submit the new version for Review after uploading everything - verifyOnly: Verifies archive with App Store Connect without uploading - rejectIfPossible: Rejects the previously submitted build if it's in a state where it's possible + - versionCheckWaitRetryLimit: After submitting a new version, App Store Connect takes some time to recognize the new version and we must wait until it's available before attempting to upload metadata for it. There is a mechanism that will check if it's available and retry with an exponential backoff if it's not available yet. This option specifies how many times we should retry before giving up. Setting this to a value below 5 is not recommended and will likely cause failures. Increase this parameter when Apple servers seem to be degraded or slow - automaticRelease: Should the app be automatically released once it's approved? (Cannot be used together with `auto_release_date`) - autoReleaseDate: Date in milliseconds for automatically releasing on pending approval (Cannot be used together with `automatic_release`) - phasedRelease: Enable the phased release feature of iTC - resetRatings: Reset the summary rating when you release a new version of the application - priceTier: The price tier of this application - appRatingConfigPath: Path to the app rating's config - - submissionInformation: Extra information for the submission (e.g. compliance specifications, IDFA settings) + - submissionInformation: Extra information for the submission (e.g. compliance specifications) - teamId: The ID of your App Store Connect team if you're in multiple teams - teamName: The name of your App Store Connect team if you're in multiple teams - devPortalTeamId: The short ID of your Developer Portal team, if you're in multiple teams. Different from your iTC team ID! @@ -3765,10 +3800,12 @@ public func deliver(apiKeyPath: OptionalConfigValue = .fastlaneDefault( skipAppVersionUpdate: OptionalConfigValue = .fastlaneDefault(deliverfile.skipAppVersionUpdate), force: OptionalConfigValue = .fastlaneDefault(deliverfile.force), overwriteScreenshots: OptionalConfigValue = .fastlaneDefault(deliverfile.overwriteScreenshots), + screenshotProcessingTimeout: Int = deliverfile.screenshotProcessingTimeout, syncScreenshots: OptionalConfigValue = .fastlaneDefault(deliverfile.syncScreenshots), submitForReview: OptionalConfigValue = .fastlaneDefault(deliverfile.submitForReview), verifyOnly: OptionalConfigValue = .fastlaneDefault(deliverfile.verifyOnly), rejectIfPossible: OptionalConfigValue = .fastlaneDefault(deliverfile.rejectIfPossible), + versionCheckWaitRetryLimit: Int = deliverfile.versionCheckWaitRetryLimit, automaticRelease: OptionalConfigValue = .fastlaneDefault(deliverfile.automaticRelease), autoReleaseDate: OptionalConfigValue = .fastlaneDefault(deliverfile.autoReleaseDate), phasedRelease: OptionalConfigValue = .fastlaneDefault(deliverfile.phasedRelease), @@ -3830,10 +3867,12 @@ public func deliver(apiKeyPath: OptionalConfigValue = .fastlaneDefault( let skipAppVersionUpdateArg = skipAppVersionUpdate.asRubyArgument(name: "skip_app_version_update", type: nil) let forceArg = force.asRubyArgument(name: "force", type: nil) let overwriteScreenshotsArg = overwriteScreenshots.asRubyArgument(name: "overwrite_screenshots", type: nil) + let screenshotProcessingTimeoutArg = RubyCommand.Argument(name: "screenshot_processing_timeout", value: screenshotProcessingTimeout, type: nil) let syncScreenshotsArg = syncScreenshots.asRubyArgument(name: "sync_screenshots", type: nil) let submitForReviewArg = submitForReview.asRubyArgument(name: "submit_for_review", type: nil) let verifyOnlyArg = verifyOnly.asRubyArgument(name: "verify_only", type: nil) let rejectIfPossibleArg = rejectIfPossible.asRubyArgument(name: "reject_if_possible", type: nil) + let versionCheckWaitRetryLimitArg = RubyCommand.Argument(name: "version_check_wait_retry_limit", value: versionCheckWaitRetryLimit, type: nil) let automaticReleaseArg = automaticRelease.asRubyArgument(name: "automatic_release", type: nil) let autoReleaseDateArg = autoReleaseDate.asRubyArgument(name: "auto_release_date", type: nil) let phasedReleaseArg = phasedRelease.asRubyArgument(name: "phased_release", type: nil) @@ -3894,10 +3933,12 @@ public func deliver(apiKeyPath: OptionalConfigValue = .fastlaneDefault( skipAppVersionUpdateArg, forceArg, overwriteScreenshotsArg, + screenshotProcessingTimeoutArg, syncScreenshotsArg, submitForReviewArg, verifyOnlyArg, rejectIfPossibleArg, + versionCheckWaitRetryLimitArg, automaticReleaseArg, autoReleaseDateArg, phasedReleaseArg, @@ -4079,7 +4120,7 @@ public func downloadAppPrivacyDetailsFromAppStore(username: String, - appIdentifier: The bundle identifier of your app - teamId: The ID of your App Store Connect team if you're in multiple teams - teamName: The name of your App Store Connect team if you're in multiple teams - - platform: The app platform for dSYMs you wish to download (ios, appletvos) + - platform: The app platform for dSYMs you wish to download (ios, xros, appletvos) - version: The app version for dSYMs you wish to download, pass in 'latest' to download only the latest build's dSYMs or 'live' to download only the live version dSYMs - buildNumber: The app build_number for dSYMs you wish to download - minVersion: The minimum app version for dSYMs you wish to download @@ -5246,17 +5287,21 @@ public func getPushCertificate(platform: String = "ios", - parameters: - path: The file(s) and path(s) you want to add - shellEscape: Shell escapes paths (set to false if using wildcards or manually escaping spaces in :path) + - force: Allow adding otherwise ignored files - pathspec: **DEPRECATED!** Use `--path` instead - The pathspec you want to add files from */ public func gitAdd(path: OptionalConfigValue<[String]?> = .fastlaneDefault(nil), shellEscape: OptionalConfigValue = .fastlaneDefault(true), + force: OptionalConfigValue = .fastlaneDefault(false), pathspec: OptionalConfigValue = .fastlaneDefault(nil)) { let pathArg = path.asRubyArgument(name: "path", type: nil) let shellEscapeArg = shellEscape.asRubyArgument(name: "shell_escape", type: nil) + let forceArg = force.asRubyArgument(name: "force", type: nil) let pathspecArg = pathspec.asRubyArgument(name: "pathspec", type: nil) let array: [RubyCommand.Argument?] = [pathArg, shellEscapeArg, + forceArg, pathspecArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } @@ -5671,6 +5716,7 @@ public func gradle(task: OptionalConfigValue = .fastlaneDefault(nil), - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file - useSystemScm: Lets xcodebuild use system's scm configuration + - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - returns: The absolute path to the generated ipa file @@ -5725,7 +5771,8 @@ public func gradle(task: OptionalConfigValue = .fastlaneDefault(nil), clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(gymfile.clonedSourcePackagesPath), skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(gymfile.skipPackageDependenciesResolution), disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(gymfile.disablePackageAutomaticUpdates), - useSystemScm: OptionalConfigValue = .fastlaneDefault(gymfile.useSystemScm)) -> String + useSystemScm: OptionalConfigValue = .fastlaneDefault(gymfile.useSystemScm), + packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(gymfile.packageAuthorizationProvider)) -> String { let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) let projectArg = project.asRubyArgument(name: "project", type: nil) @@ -5777,6 +5824,7 @@ public func gradle(task: OptionalConfigValue = .fastlaneDefault(nil), let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) + let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) let array: [RubyCommand.Argument?] = [workspaceArg, projectArg, schemeArg, @@ -5826,7 +5874,8 @@ public func gradle(task: OptionalConfigValue = .fastlaneDefault(nil), clonedSourcePackagesPathArg, skipPackageDependenciesResolutionArg, disablePackageAutomaticUpdatesArg, - useSystemScmArg] + useSystemScmArg, + packageAuthorizationProviderArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } .compactMap { $0 } @@ -6774,6 +6823,7 @@ public func makeChangelogFromJenkins(fallbackChangelog: String = "", - skipCertificateMatching: Set to true if there is no access to Apple developer portal but there are certificates, keys and profiles provided. Only works with match import action - outputPath: Path in which to export certificates, key and profile - skipSetPartitionList: Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing + - forceLegacyEncryption: Force encryption to use legacy cbc algorithm for backwards compatibility with older match versions - verbose: Print out extra information and all commands More information: https://docs.fastlane.tools/actions/match/ @@ -6832,6 +6882,7 @@ public func match(type: String = matchfile.type, skipCertificateMatching: OptionalConfigValue = .fastlaneDefault(matchfile.skipCertificateMatching), outputPath: OptionalConfigValue = .fastlaneDefault(matchfile.outputPath), skipSetPartitionList: OptionalConfigValue = .fastlaneDefault(matchfile.skipSetPartitionList), + forceLegacyEncryption: OptionalConfigValue = .fastlaneDefault(matchfile.forceLegacyEncryption), verbose: OptionalConfigValue = .fastlaneDefault(matchfile.verbose)) { let typeArg = RubyCommand.Argument(name: "type", value: type, type: nil) @@ -6888,6 +6939,7 @@ public func match(type: String = matchfile.type, let skipCertificateMatchingArg = skipCertificateMatching.asRubyArgument(name: "skip_certificate_matching", type: nil) let outputPathArg = outputPath.asRubyArgument(name: "output_path", type: nil) let skipSetPartitionListArg = skipSetPartitionList.asRubyArgument(name: "skip_set_partition_list", type: nil) + let forceLegacyEncryptionArg = forceLegacyEncryption.asRubyArgument(name: "force_legacy_encryption", type: nil) let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) let array: [RubyCommand.Argument?] = [typeArg, additionalCertTypesArg, @@ -6943,6 +6995,7 @@ public func match(type: String = matchfile.type, skipCertificateMatchingArg, outputPathArg, skipSetPartitionListArg, + forceLegacyEncryptionArg, verboseArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } @@ -7009,6 +7062,7 @@ public func match(type: String = matchfile.type, - skipCertificateMatching: Set to true if there is no access to Apple developer portal but there are certificates, keys and profiles provided. Only works with match import action - outputPath: Path in which to export certificates, key and profile - skipSetPartitionList: Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing + - forceLegacyEncryption: Force encryption to use legacy cbc algorithm for backwards compatibility with older match versions - verbose: Print out extra information and all commands Use the match_nuke action to revoke your certificates and provisioning profiles. @@ -7071,6 +7125,7 @@ public func matchNuke(type: String = "development", skipCertificateMatching: OptionalConfigValue = .fastlaneDefault(false), outputPath: OptionalConfigValue = .fastlaneDefault(nil), skipSetPartitionList: OptionalConfigValue = .fastlaneDefault(false), + forceLegacyEncryption: OptionalConfigValue = .fastlaneDefault(false), verbose: OptionalConfigValue = .fastlaneDefault(false)) { let typeArg = RubyCommand.Argument(name: "type", value: type, type: nil) @@ -7127,6 +7182,7 @@ public func matchNuke(type: String = "development", let skipCertificateMatchingArg = skipCertificateMatching.asRubyArgument(name: "skip_certificate_matching", type: nil) let outputPathArg = outputPath.asRubyArgument(name: "output_path", type: nil) let skipSetPartitionListArg = skipSetPartitionList.asRubyArgument(name: "skip_set_partition_list", type: nil) + let forceLegacyEncryptionArg = forceLegacyEncryption.asRubyArgument(name: "force_legacy_encryption", type: nil) let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) let array: [RubyCommand.Argument?] = [typeArg, additionalCertTypesArg, @@ -7182,6 +7238,7 @@ public func matchNuke(type: String = "development", skipCertificateMatchingArg, outputPathArg, skipSetPartitionListArg, + forceLegacyEncryptionArg, verboseArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } @@ -7552,6 +7609,7 @@ public func oclint(oclintPath: String = "oclint", - appName: OneSignal App Name. This is required when creating an app (in other words, when `:app_id` is not set, and optional when updating an app - androidToken: ANDROID GCM KEY - androidGcmSenderId: GCM SENDER ID + - fcmJson: FCM Service Account JSON File (in .json format) - apnsP12: APNS P12 File (in .p12 format) - apnsP12Password: APNS P12 password - apnsEnv: APNS environment @@ -7564,6 +7622,7 @@ public func onesignal(appId: OptionalConfigValue = .fastlaneDefault(nil appName: OptionalConfigValue = .fastlaneDefault(nil), androidToken: OptionalConfigValue = .fastlaneDefault(nil), androidGcmSenderId: OptionalConfigValue = .fastlaneDefault(nil), + fcmJson: OptionalConfigValue = .fastlaneDefault(nil), apnsP12: OptionalConfigValue = .fastlaneDefault(nil), apnsP12Password: OptionalConfigValue = .fastlaneDefault(nil), apnsEnv: String = "production", @@ -7574,6 +7633,7 @@ public func onesignal(appId: OptionalConfigValue = .fastlaneDefault(nil let appNameArg = appName.asRubyArgument(name: "app_name", type: nil) let androidTokenArg = androidToken.asRubyArgument(name: "android_token", type: nil) let androidGcmSenderIdArg = androidGcmSenderId.asRubyArgument(name: "android_gcm_sender_id", type: nil) + let fcmJsonArg = fcmJson.asRubyArgument(name: "fcm_json", type: nil) let apnsP12Arg = apnsP12.asRubyArgument(name: "apns_p12", type: nil) let apnsP12PasswordArg = apnsP12Password.asRubyArgument(name: "apns_p12_password", type: nil) let apnsEnvArg = RubyCommand.Argument(name: "apns_env", value: apnsEnv, type: nil) @@ -7583,6 +7643,7 @@ public func onesignal(appId: OptionalConfigValue = .fastlaneDefault(nil appNameArg, androidTokenArg, androidGcmSenderIdArg, + fcmJsonArg, apnsP12Arg, apnsP12PasswordArg, apnsEnvArg, @@ -8829,6 +8890,7 @@ public func rubyVersion() { - useSystemScm: Lets xcodebuild use system's scm configuration - numberOfRetries: The number of times a test can fail - failBuild: Should this step stop the build if the tests fail? Set this to false if you're using trainer + - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - returns: Outputs hash of results with the following keys: :number_of_tests, :number_of_failures, :number_of_retries, :number_of_tests_excluding_retries, :number_of_failures_excluding_retries @@ -8911,7 +8973,8 @@ public func rubyVersion() { disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(false), useSystemScm: OptionalConfigValue = .fastlaneDefault(false), numberOfRetries: Int = 0, - failBuild: OptionalConfigValue = .fastlaneDefault(true)) -> [String: Any] + failBuild: OptionalConfigValue = .fastlaneDefault(true), + packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(nil)) -> [String: Any] { let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) let projectArg = project.asRubyArgument(name: "project", type: nil) @@ -8991,6 +9054,7 @@ public func rubyVersion() { let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) let numberOfRetriesArg = RubyCommand.Argument(name: "number_of_retries", value: numberOfRetries, type: nil) let failBuildArg = failBuild.asRubyArgument(name: "fail_build", type: nil) + let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) let array: [RubyCommand.Argument?] = [workspaceArg, projectArg, packagePathArg, @@ -9068,7 +9132,8 @@ public func rubyVersion() { disablePackageAutomaticUpdatesArg, useSystemScmArg, numberOfRetriesArg, - failBuildArg] + failBuildArg, + packageAuthorizationProviderArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } .compactMap { $0 } @@ -9260,6 +9325,7 @@ public func say(text: [String], - useSystemScm: Lets xcodebuild use system's scm configuration - numberOfRetries: The number of times a test can fail - failBuild: Should this step stop the build if the tests fail? Set this to false if you're using trainer + - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - returns: Outputs hash of results with the following keys: :number_of_tests, :number_of_failures, :number_of_retries, :number_of_tests_excluding_retries, :number_of_failures_excluding_retries @@ -9342,7 +9408,8 @@ public func say(text: [String], disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(scanfile.disablePackageAutomaticUpdates), useSystemScm: OptionalConfigValue = .fastlaneDefault(scanfile.useSystemScm), numberOfRetries: Int = scanfile.numberOfRetries, - failBuild: OptionalConfigValue = .fastlaneDefault(scanfile.failBuild)) -> [String: Any] + failBuild: OptionalConfigValue = .fastlaneDefault(scanfile.failBuild), + packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(scanfile.packageAuthorizationProvider)) -> [String: Any] { let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) let projectArg = project.asRubyArgument(name: "project", type: nil) @@ -9422,6 +9489,7 @@ public func say(text: [String], let useSystemScmArg = useSystemScm.asRubyArgument(name: "use_system_scm", type: nil) let numberOfRetriesArg = RubyCommand.Argument(name: "number_of_retries", value: numberOfRetries, type: nil) let failBuildArg = failBuild.asRubyArgument(name: "fail_build", type: nil) + let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) let array: [RubyCommand.Argument?] = [workspaceArg, projectArg, packagePathArg, @@ -9499,7 +9567,8 @@ public func say(text: [String], disablePackageAutomaticUpdatesArg, useSystemScmArg, numberOfRetriesArg, - failBuildArg] + failBuildArg, + packageAuthorizationProviderArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } .compactMap { $0 } @@ -9682,7 +9751,7 @@ public func setBuildNumberRepository(useHgRevisionNumber: OptionalConfigValue = .fastlaneDefault(nil), slackUrl: String, username: String = "fastlane", iconUrl: String = "https://fastlane.tools/assets/img/fastlane_icon.png", + iconEmoji: OptionalConfigValue = .fastlaneDefault(nil), payload: [String: Any] = [:], defaultPayloads: [String] = ["lane", "test_result", "git_branch", "git_author", "last_git_commit", "last_git_commit_hash"], attachmentProperties: [String: Any] = [:], @@ -10211,6 +10282,7 @@ public func slack(message: OptionalConfigValue = .fastlaneDefault(nil), let slackUrlArg = RubyCommand.Argument(name: "slack_url", value: slackUrl, type: nil) let usernameArg = RubyCommand.Argument(name: "username", value: username, type: nil) let iconUrlArg = RubyCommand.Argument(name: "icon_url", value: iconUrl, type: nil) + let iconEmojiArg = iconEmoji.asRubyArgument(name: "icon_emoji", type: nil) let payloadArg = RubyCommand.Argument(name: "payload", value: payload, type: nil) let defaultPayloadsArg = RubyCommand.Argument(name: "default_payloads", value: defaultPayloads, type: nil) let attachmentPropertiesArg = RubyCommand.Argument(name: "attachment_properties", value: attachmentProperties, type: nil) @@ -10224,6 +10296,7 @@ public func slack(message: OptionalConfigValue = .fastlaneDefault(nil), slackUrlArg, usernameArg, iconUrlArg, + iconEmojiArg, payloadArg, defaultPayloadsArg, attachmentPropertiesArg, @@ -10438,8 +10511,8 @@ public func slather(buildDirectory: OptionalConfigValue = .fastlaneDefa Alias for the `capture_ios_screenshots` action - parameters: - - workspace: Path the workspace file - - project: Path the project file + - workspace: Path to the workspace file + - project: Path to the project file - xcargs: Pass additional arguments to xcodebuild for the test phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" - xcconfig: Use an extra XCCONFIG file to build your app - devices: A list of devices you want to take the screenshots from @@ -10479,6 +10552,7 @@ public func slather(buildDirectory: OptionalConfigValue = .fastlaneDefa - clonedSourcePackagesPath: Sets a custom path for Swift Package Manager dependencies - skipPackageDependenciesResolution: Skips resolution of Swift Package Manager dependencies - disablePackageAutomaticUpdates: Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file + - packageAuthorizationProvider: Lets xcodebuild use a specified package authorization provider (keychain|netrc) - testplan: The testplan associated with the scheme that should be used for testing - onlyTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to run - skipTesting: Array of strings matching Test Bundle/Test Suite/Test Cases to skip @@ -10529,6 +10603,7 @@ public func snapshot(workspace: OptionalConfigValue = .fastlaneDefault( clonedSourcePackagesPath: OptionalConfigValue = .fastlaneDefault(snapshotfile.clonedSourcePackagesPath), skipPackageDependenciesResolution: OptionalConfigValue = .fastlaneDefault(snapshotfile.skipPackageDependenciesResolution), disablePackageAutomaticUpdates: OptionalConfigValue = .fastlaneDefault(snapshotfile.disablePackageAutomaticUpdates), + packageAuthorizationProvider: OptionalConfigValue = .fastlaneDefault(snapshotfile.packageAuthorizationProvider), testplan: OptionalConfigValue = .fastlaneDefault(snapshotfile.testplan), onlyTesting: Any? = snapshotfile.onlyTesting, skipTesting: Any? = snapshotfile.skipTesting, @@ -10579,6 +10654,7 @@ public func snapshot(workspace: OptionalConfigValue = .fastlaneDefault( let clonedSourcePackagesPathArg = clonedSourcePackagesPath.asRubyArgument(name: "cloned_source_packages_path", type: nil) let skipPackageDependenciesResolutionArg = skipPackageDependenciesResolution.asRubyArgument(name: "skip_package_dependencies_resolution", type: nil) let disablePackageAutomaticUpdatesArg = disablePackageAutomaticUpdates.asRubyArgument(name: "disable_package_automatic_updates", type: nil) + let packageAuthorizationProviderArg = packageAuthorizationProvider.asRubyArgument(name: "package_authorization_provider", type: nil) let testplanArg = testplan.asRubyArgument(name: "testplan", type: nil) let onlyTestingArg = RubyCommand.Argument(name: "only_testing", value: onlyTesting, type: nil) let skipTestingArg = RubyCommand.Argument(name: "skip_testing", value: skipTesting, type: nil) @@ -10628,6 +10704,7 @@ public func snapshot(workspace: OptionalConfigValue = .fastlaneDefault( clonedSourcePackagesPathArg, skipPackageDependenciesResolutionArg, disablePackageAutomaticUpdatesArg, + packageAuthorizationProviderArg, testplanArg, onlyTestingArg, skipTestingArg, @@ -10913,6 +10990,7 @@ public func splunkmint(dsym: OptionalConfigValue = .fastlaneDefault(nil - xcprettyOutput: Specifies the output type for xcpretty. eg. 'test', or 'simple' - xcprettyArgs: Pass in xcpretty additional command line arguments (e.g. '--test --no-color' or '--tap --no-utf'), requires xcpretty_output to be specified also - verbose: Increase verbosity of informational output + - veryVerbose: Increase verbosity to include debug output - simulator: Specifies the simulator to pass for Swift Compiler (one of: iphonesimulator, macosx) - simulatorArch: Specifies the architecture of the simulator to pass for Swift Compiler (one of: x86_64, arm64). Requires the simulator option to be specified also, otherwise, it's ignored */ @@ -10928,6 +11006,7 @@ public func spm(command: String = "build", xcprettyOutput: OptionalConfigValue = .fastlaneDefault(nil), xcprettyArgs: OptionalConfigValue = .fastlaneDefault(nil), verbose: OptionalConfigValue = .fastlaneDefault(false), + veryVerbose: OptionalConfigValue = .fastlaneDefault(false), simulator: OptionalConfigValue = .fastlaneDefault(nil), simulatorArch: String = "arm64") { @@ -10943,6 +11022,7 @@ public func spm(command: String = "build", let xcprettyOutputArg = xcprettyOutput.asRubyArgument(name: "xcpretty_output", type: nil) let xcprettyArgsArg = xcprettyArgs.asRubyArgument(name: "xcpretty_args", type: nil) let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) + let veryVerboseArg = veryVerbose.asRubyArgument(name: "very_verbose", type: nil) let simulatorArg = simulator.asRubyArgument(name: "simulator", type: nil) let simulatorArchArg = RubyCommand.Argument(name: "simulator_arch", value: simulatorArch, type: nil) let array: [RubyCommand.Argument?] = [commandArg, @@ -10957,6 +11037,7 @@ public func spm(command: String = "build", xcprettyOutputArg, xcprettyArgsArg, verboseArg, + veryVerboseArg, simulatorArg, simulatorArchArg] let args: [RubyCommand.Argument] = array @@ -11307,6 +11388,7 @@ public func swiftlint(mode: String = "lint", - skipCertificateMatching: Set to true if there is no access to Apple developer portal but there are certificates, keys and profiles provided. Only works with match import action - outputPath: Path in which to export certificates, key and profile - skipSetPartitionList: Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing + - forceLegacyEncryption: Force encryption to use legacy cbc algorithm for backwards compatibility with older match versions - verbose: Print out extra information and all commands More information: https://docs.fastlane.tools/actions/match/ @@ -11365,6 +11447,7 @@ public func syncCodeSigning(type: String = "development", skipCertificateMatching: OptionalConfigValue = .fastlaneDefault(false), outputPath: OptionalConfigValue = .fastlaneDefault(nil), skipSetPartitionList: OptionalConfigValue = .fastlaneDefault(false), + forceLegacyEncryption: OptionalConfigValue = .fastlaneDefault(false), verbose: OptionalConfigValue = .fastlaneDefault(false)) { let typeArg = RubyCommand.Argument(name: "type", value: type, type: nil) @@ -11421,6 +11504,7 @@ public func syncCodeSigning(type: String = "development", let skipCertificateMatchingArg = skipCertificateMatching.asRubyArgument(name: "skip_certificate_matching", type: nil) let outputPathArg = outputPath.asRubyArgument(name: "output_path", type: nil) let skipSetPartitionListArg = skipSetPartitionList.asRubyArgument(name: "skip_set_partition_list", type: nil) + let forceLegacyEncryptionArg = forceLegacyEncryption.asRubyArgument(name: "force_legacy_encryption", type: nil) let verboseArg = verbose.asRubyArgument(name: "verbose", type: nil) let array: [RubyCommand.Argument?] = [typeArg, additionalCertTypesArg, @@ -11476,6 +11560,7 @@ public func syncCodeSigning(type: String = "development", skipCertificateMatchingArg, outputPathArg, skipSetPartitionListArg, + forceLegacyEncryptionArg, verboseArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } @@ -11519,6 +11604,7 @@ public func teamName() { - options: Array of options (shake,video_only_wifi,anonymous) - custom: Array of custom options. Contact support@testfairy.com for more information - timeout: Request timeout in seconds + - tags: Custom tags that can be used to organize your builds You can retrieve your API key on [your settings page](https://free.testfairy.com/settings/) */ @@ -11534,7 +11620,8 @@ public func testfairy(apiKey: String, notify: String = "off", options: [String] = [], custom: String = "", - timeout: OptionalConfigValue = .fastlaneDefault(nil)) + timeout: OptionalConfigValue = .fastlaneDefault(nil), + tags: [String] = []) { let apiKeyArg = RubyCommand.Argument(name: "api_key", value: apiKey, type: nil) let ipaArg = ipa.asRubyArgument(name: "ipa", type: nil) @@ -11549,6 +11636,7 @@ public func testfairy(apiKey: String, let optionsArg = RubyCommand.Argument(name: "options", value: options, type: nil) let customArg = RubyCommand.Argument(name: "custom", value: custom, type: nil) let timeoutArg = timeout.asRubyArgument(name: "timeout", type: nil) + let tagsArg = RubyCommand.Argument(name: "tags", value: tags, type: nil) let array: [RubyCommand.Argument?] = [apiKeyArg, ipaArg, apkArg, @@ -11561,7 +11649,8 @@ public func testfairy(apiKey: String, notifyArg, optionsArg, customArg, - timeoutArg] + timeoutArg, + tagsArg] let args: [RubyCommand.Argument] = array .filter { $0?.value != nil } .compactMap { $0 } @@ -12499,17 +12588,19 @@ public func uploadSymbolsToSentry(apiHost: String = "https://app.getsentry.com/a - skipAppVersionUpdate: Don’t create or update the app version that is being prepared for submission - force: Skip verification of HTML preview file - overwriteScreenshots: Clear all previously uploaded screenshots before uploading the new ones + - screenshotProcessingTimeout: Timeout in seconds to wait before considering screenshot processing as failed, used to handle cases where uploads to the App Store are stuck in processing - syncScreenshots: Sync screenshots with local ones. This is currently beta option so set true to 'FASTLANE_ENABLE_BETA_DELIVER_SYNC_SCREENSHOTS' environment variable as well - submitForReview: Submit the new version for Review after uploading everything - verifyOnly: Verifies archive with App Store Connect without uploading - rejectIfPossible: Rejects the previously submitted build if it's in a state where it's possible + - versionCheckWaitRetryLimit: After submitting a new version, App Store Connect takes some time to recognize the new version and we must wait until it's available before attempting to upload metadata for it. There is a mechanism that will check if it's available and retry with an exponential backoff if it's not available yet. This option specifies how many times we should retry before giving up. Setting this to a value below 5 is not recommended and will likely cause failures. Increase this parameter when Apple servers seem to be degraded or slow - automaticRelease: Should the app be automatically released once it's approved? (Cannot be used together with `auto_release_date`) - autoReleaseDate: Date in milliseconds for automatically releasing on pending approval (Cannot be used together with `automatic_release`) - phasedRelease: Enable the phased release feature of iTC - resetRatings: Reset the summary rating when you release a new version of the application - priceTier: The price tier of this application - appRatingConfigPath: Path to the app rating's config - - submissionInformation: Extra information for the submission (e.g. compliance specifications, IDFA settings) + - submissionInformation: Extra information for the submission (e.g. compliance specifications) - teamId: The ID of your App Store Connect team if you're in multiple teams - teamName: The name of your App Store Connect team if you're in multiple teams - devPortalTeamId: The short ID of your Developer Portal team, if you're in multiple teams. Different from your iTC team ID! @@ -12571,10 +12662,12 @@ public func uploadToAppStore(apiKeyPath: OptionalConfigValue = .fastlan skipAppVersionUpdate: OptionalConfigValue = .fastlaneDefault(false), force: OptionalConfigValue = .fastlaneDefault(false), overwriteScreenshots: OptionalConfigValue = .fastlaneDefault(false), + screenshotProcessingTimeout: Int = 3600, syncScreenshots: OptionalConfigValue = .fastlaneDefault(false), submitForReview: OptionalConfigValue = .fastlaneDefault(false), verifyOnly: OptionalConfigValue = .fastlaneDefault(false), rejectIfPossible: OptionalConfigValue = .fastlaneDefault(false), + versionCheckWaitRetryLimit: Int = 7, automaticRelease: OptionalConfigValue = .fastlaneDefault(nil), autoReleaseDate: OptionalConfigValue = .fastlaneDefault(nil), phasedRelease: OptionalConfigValue = .fastlaneDefault(false), @@ -12636,10 +12729,12 @@ public func uploadToAppStore(apiKeyPath: OptionalConfigValue = .fastlan let skipAppVersionUpdateArg = skipAppVersionUpdate.asRubyArgument(name: "skip_app_version_update", type: nil) let forceArg = force.asRubyArgument(name: "force", type: nil) let overwriteScreenshotsArg = overwriteScreenshots.asRubyArgument(name: "overwrite_screenshots", type: nil) + let screenshotProcessingTimeoutArg = RubyCommand.Argument(name: "screenshot_processing_timeout", value: screenshotProcessingTimeout, type: nil) let syncScreenshotsArg = syncScreenshots.asRubyArgument(name: "sync_screenshots", type: nil) let submitForReviewArg = submitForReview.asRubyArgument(name: "submit_for_review", type: nil) let verifyOnlyArg = verifyOnly.asRubyArgument(name: "verify_only", type: nil) let rejectIfPossibleArg = rejectIfPossible.asRubyArgument(name: "reject_if_possible", type: nil) + let versionCheckWaitRetryLimitArg = RubyCommand.Argument(name: "version_check_wait_retry_limit", value: versionCheckWaitRetryLimit, type: nil) let automaticReleaseArg = automaticRelease.asRubyArgument(name: "automatic_release", type: nil) let autoReleaseDateArg = autoReleaseDate.asRubyArgument(name: "auto_release_date", type: nil) let phasedReleaseArg = phasedRelease.asRubyArgument(name: "phased_release", type: nil) @@ -12700,10 +12795,12 @@ public func uploadToAppStore(apiKeyPath: OptionalConfigValue = .fastlan skipAppVersionUpdateArg, forceArg, overwriteScreenshotsArg, + screenshotProcessingTimeoutArg, syncScreenshotsArg, submitForReviewArg, verifyOnlyArg, rejectIfPossibleArg, + versionCheckWaitRetryLimitArg, automaticReleaseArg, autoReleaseDateArg, phasedReleaseArg, @@ -13561,7 +13658,7 @@ public func xcov(workspace: OptionalConfigValue = .fastlaneDefault(nil) coverallsServiceJobId: OptionalConfigValue = .fastlaneDefault(nil), coverallsRepoToken: OptionalConfigValue = .fastlaneDefault(nil), xcconfig: OptionalConfigValue = .fastlaneDefault(nil), - ideFoundationPath: String = "/Applications/Xcode-15.1.0.app/Contents/Developer/../Frameworks/IDEFoundation.framework/Versions/A/IDEFoundation", + ideFoundationPath: String = "/Applications/Xcode_15.4.app/Contents/Developer/../Frameworks/IDEFoundation.framework/Versions/A/IDEFoundation", legacySupport: OptionalConfigValue = .fastlaneDefault(false)) { let workspaceArg = workspace.asRubyArgument(name: "workspace", type: nil) @@ -13764,4 +13861,4 @@ public let snapshotfile: Snapshotfile = .init() // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.176] +// FastlaneRunnerAPIVersion [0.9.182] diff --git a/fastlane/swift/Gymfile.swift b/fastlane/swift/Gymfile.swift index 74bfdf83413..bfe7d8a502b 100644 --- a/fastlane/swift/Gymfile.swift +++ b/fastlane/swift/Gymfile.swift @@ -17,4 +17,4 @@ public class Gymfile: GymfileProtocol { // during the `init` process, and you won't see this message } -// Generated with fastlane 2.219.0 +// Generated with fastlane 2.223.1 diff --git a/fastlane/swift/GymfileProtocol.swift b/fastlane/swift/GymfileProtocol.swift index c97a4498299..82f2c7df6c2 100644 --- a/fastlane/swift/GymfileProtocol.swift +++ b/fastlane/swift/GymfileProtocol.swift @@ -151,6 +151,9 @@ public protocol GymfileProtocol: AnyObject { /// Lets xcodebuild use system's scm configuration var useSystemScm: Bool { get } + + /// Lets xcodebuild use a specified package authorization provider (keychain|netrc) + var packageAuthorizationProvider: String? { get } } public extension GymfileProtocol { @@ -204,8 +207,9 @@ public extension GymfileProtocol { var skipPackageDependenciesResolution: Bool { return false } var disablePackageAutomaticUpdates: Bool { return false } var useSystemScm: Bool { return false } + var packageAuthorizationProvider: String? { return nil } } // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.126] +// FastlaneRunnerAPIVersion [0.9.132] diff --git a/fastlane/swift/LaneFileProtocol.swift b/fastlane/swift/LaneFileProtocol.swift index f653476c12d..a4681537529 100644 --- a/fastlane/swift/LaneFileProtocol.swift +++ b/fastlane/swift/LaneFileProtocol.swift @@ -68,7 +68,7 @@ open class LaneFile: NSObject, LaneFileProtocol { public static var lanes: [String: String] { var laneToMethodName: [String: String] = [:] - laneFunctionNames.forEach { name in + for name in laneFunctionNames { let lowercasedName = name.lowercased() if lowercasedName.hasSuffix("lane") { laneToMethodName[lowercasedName] = name diff --git a/fastlane/swift/Matchfile.swift b/fastlane/swift/Matchfile.swift index db2792d970a..c1626537ba0 100644 --- a/fastlane/swift/Matchfile.swift +++ b/fastlane/swift/Matchfile.swift @@ -17,4 +17,4 @@ public class Matchfile: MatchfileProtocol { // during the `init` process, and you won't see this message } -// Generated with fastlane 2.219.0 +// Generated with fastlane 2.223.1 diff --git a/fastlane/swift/MatchfileProtocol.swift b/fastlane/swift/MatchfileProtocol.swift index d9fa9e7610c..2297cdc3272 100644 --- a/fastlane/swift/MatchfileProtocol.swift +++ b/fastlane/swift/MatchfileProtocol.swift @@ -164,6 +164,9 @@ public protocol MatchfileProtocol: AnyObject { /// Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing var skipSetPartitionList: Bool { get } + /// Force encryption to use legacy cbc algorithm for backwards compatibility with older match versions + var forceLegacyEncryption: Bool { get } + /// Print out extra information and all commands var verbose: Bool { get } } @@ -223,9 +226,10 @@ public extension MatchfileProtocol { var skipCertificateMatching: Bool { return false } var outputPath: String? { return nil } var skipSetPartitionList: Bool { return false } + var forceLegacyEncryption: Bool { return false } var verbose: Bool { return false } } // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.120] +// FastlaneRunnerAPIVersion [0.9.126] diff --git a/fastlane/swift/OptionalConfigValue.swift b/fastlane/swift/OptionalConfigValue.swift index dfa848a6a53..6afc847d03c 100644 --- a/fastlane/swift/OptionalConfigValue.swift +++ b/fastlane/swift/OptionalConfigValue.swift @@ -93,8 +93,8 @@ extension OptionalConfigValue: ExpressibleByDictionaryLiteral where T == [String public init(dictionaryLiteral elements: (Key, Value)...) { var dict: [Key: Value] = [:] - elements.forEach { - dict[$0.0] = $0.1 + for element in elements { + dict[element.0] = element.1 } self = .userDefined(dict) } diff --git a/fastlane/swift/Precheckfile.swift b/fastlane/swift/Precheckfile.swift index c2590b0ffca..a6025b7f626 100644 --- a/fastlane/swift/Precheckfile.swift +++ b/fastlane/swift/Precheckfile.swift @@ -17,4 +17,4 @@ public class Precheckfile: PrecheckfileProtocol { // during the `init` process, and you won't see this message } -// Generated with fastlane 2.219.0 +// Generated with fastlane 2.223.1 diff --git a/fastlane/swift/PrecheckfileProtocol.swift b/fastlane/swift/PrecheckfileProtocol.swift index 8b2598cbe3f..01002a9f39c 100644 --- a/fastlane/swift/PrecheckfileProtocol.swift +++ b/fastlane/swift/PrecheckfileProtocol.swift @@ -52,4 +52,4 @@ public extension PrecheckfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.119] +// FastlaneRunnerAPIVersion [0.9.125] diff --git a/fastlane/swift/Scanfile.swift b/fastlane/swift/Scanfile.swift index a999d4ea34b..cf005f7511b 100644 --- a/fastlane/swift/Scanfile.swift +++ b/fastlane/swift/Scanfile.swift @@ -17,4 +17,4 @@ public class Scanfile: ScanfileProtocol { // during the `init` process, and you won't see this message } -// Generated with fastlane 2.219.0 +// Generated with fastlane 2.223.1 diff --git a/fastlane/swift/ScanfileProtocol.swift b/fastlane/swift/ScanfileProtocol.swift index ffaeedbb496..68f63711e69 100644 --- a/fastlane/swift/ScanfileProtocol.swift +++ b/fastlane/swift/ScanfileProtocol.swift @@ -235,6 +235,9 @@ public protocol ScanfileProtocol: AnyObject { /// Should this step stop the build if the tests fail? Set this to false if you're using trainer var failBuild: Bool { get } + + /// Lets xcodebuild use a specified package authorization provider (keychain|netrc) + var packageAuthorizationProvider: String? { get } } public extension ScanfileProtocol { @@ -316,8 +319,9 @@ public extension ScanfileProtocol { var useSystemScm: Bool { return false } var numberOfRetries: Int { return 0 } var failBuild: Bool { return true } + var packageAuthorizationProvider: String? { return nil } } // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.131] +// FastlaneRunnerAPIVersion [0.9.137] diff --git a/fastlane/swift/Screengrabfile.swift b/fastlane/swift/Screengrabfile.swift index 20e55113834..7566ef9f531 100644 --- a/fastlane/swift/Screengrabfile.swift +++ b/fastlane/swift/Screengrabfile.swift @@ -17,4 +17,4 @@ public class Screengrabfile: ScreengrabfileProtocol { // during the `init` process, and you won't see this message } -// Generated with fastlane 2.219.0 +// Generated with fastlane 2.223.1 diff --git a/fastlane/swift/ScreengrabfileProtocol.swift b/fastlane/swift/ScreengrabfileProtocol.swift index effc625c6e9..f7971cdc350 100644 --- a/fastlane/swift/ScreengrabfileProtocol.swift +++ b/fastlane/swift/ScreengrabfileProtocol.swift @@ -96,4 +96,4 @@ public extension ScreengrabfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.121] +// FastlaneRunnerAPIVersion [0.9.127] diff --git a/fastlane/swift/Snapshotfile.swift b/fastlane/swift/Snapshotfile.swift index e8c18144ad7..1993c74df4f 100644 --- a/fastlane/swift/Snapshotfile.swift +++ b/fastlane/swift/Snapshotfile.swift @@ -17,4 +17,4 @@ public class Snapshotfile: SnapshotfileProtocol { // during the `init` process, and you won't see this message } -// Generated with fastlane 2.219.0 +// Generated with fastlane 2.223.1 diff --git a/fastlane/swift/SnapshotfileProtocol.swift b/fastlane/swift/SnapshotfileProtocol.swift index e7132923a0e..82c55bd9f2e 100644 --- a/fastlane/swift/SnapshotfileProtocol.swift +++ b/fastlane/swift/SnapshotfileProtocol.swift @@ -2,10 +2,10 @@ // Copyright (c) 2024 FastlaneTools public protocol SnapshotfileProtocol: AnyObject { - /// Path the workspace file + /// Path to the workspace file var workspace: String? { get } - /// Path the project file + /// Path to the project file var project: String? { get } /// Pass additional arguments to xcodebuild for the test phase. Be sure to quote the setting names and values e.g. OTHER_LDFLAGS="-ObjC -lstdc++" @@ -125,6 +125,9 @@ public protocol SnapshotfileProtocol: AnyObject { /// Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file var disablePackageAutomaticUpdates: Bool { get } + /// Lets xcodebuild use a specified package authorization provider (keychain|netrc) + var packageAuthorizationProvider: String? { get } + /// The testplan associated with the scheme that should be used for testing var testplan: String? { get } @@ -192,6 +195,7 @@ public extension SnapshotfileProtocol { var clonedSourcePackagesPath: String? { return nil } var skipPackageDependenciesResolution: Bool { return false } var disablePackageAutomaticUpdates: Bool { return false } + var packageAuthorizationProvider: String? { return nil } var testplan: String? { return nil } var onlyTesting: String? { return nil } var skipTesting: String? { return nil } @@ -204,4 +208,4 @@ public extension SnapshotfileProtocol { // Please don't remove the lines below // They are used to detect outdated files -// FastlaneRunnerAPIVersion [0.9.115] +// FastlaneRunnerAPIVersion [0.9.121] diff --git a/fastlane/swift/formatting/Brewfile.lock.json b/fastlane/swift/formatting/Brewfile.lock.json index c3d524f440b..8efe0cb7c0e 100644 --- a/fastlane/swift/formatting/Brewfile.lock.json +++ b/fastlane/swift/formatting/Brewfile.lock.json @@ -2,45 +2,50 @@ "entries": { "brew": { "swiftformat": { - "version": "0.52.11", + "version": "0.54.5", "bottle": { "rebuild": 0, "root_url": "https://ghcr.io/v2/homebrew/core", "files": { + "arm64_sequoia": { + "cellar": ":any_skip_relocation", + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:3fc57cb9abcbfd64106a3b16f51c8851c9877327553ec5fd9b21683d42b3c18d", + "sha256": "3fc57cb9abcbfd64106a3b16f51c8851c9877327553ec5fd9b21683d42b3c18d" + }, "arm64_sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:b1dabab20931536f6cb157767e6e732ecbabb093ffaaae7325f9971f3c6b6db5", - "sha256": "b1dabab20931536f6cb157767e6e732ecbabb093ffaaae7325f9971f3c6b6db5" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:4d1f7565498827bbc53230f01c2fca4a7d082f4ae16d32ae568ba633c090c6ee", + "sha256": "4d1f7565498827bbc53230f01c2fca4a7d082f4ae16d32ae568ba633c090c6ee" }, "arm64_ventura": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:2674f87602a4a6d6af2f6ee822f7e36eb09c65e4bc31448d79d8955981645b1c", - "sha256": "2674f87602a4a6d6af2f6ee822f7e36eb09c65e4bc31448d79d8955981645b1c" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:416b54dc7938754980f9b2d732254ce7a36c401c2df3b68eba47f54db9bb956c", + "sha256": "416b54dc7938754980f9b2d732254ce7a36c401c2df3b68eba47f54db9bb956c" }, "arm64_monterey": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:1581f87673144bd34caa286b236e0c9d9cae42c20f5ff353ec7d139d03b63fd3", - "sha256": "1581f87673144bd34caa286b236e0c9d9cae42c20f5ff353ec7d139d03b63fd3" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:3b14b0bacb0938c650e2d0d30d1f546ea7bac4feac510be16f09a89abd9f95d4", + "sha256": "3b14b0bacb0938c650e2d0d30d1f546ea7bac4feac510be16f09a89abd9f95d4" }, "sonoma": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:d10f7a49255b390e1f8f174947427ae12b3d60477071b6922f0ae59579bebf16", - "sha256": "d10f7a49255b390e1f8f174947427ae12b3d60477071b6922f0ae59579bebf16" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:445a0e38bda1f1dbda7c34dbb75a1b4432c5f0de7f9fa8bd4e03a9220b9bda19", + "sha256": "445a0e38bda1f1dbda7c34dbb75a1b4432c5f0de7f9fa8bd4e03a9220b9bda19" }, "ventura": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:8505abd9424d6bc0b992302fcb319aea05119eb2397ccc6777c09e6932364c74", - "sha256": "8505abd9424d6bc0b992302fcb319aea05119eb2397ccc6777c09e6932364c74" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:508b2e8000078773c7884e17b8d1ca711f4313ba14ff0000f7a28af68b02e71a", + "sha256": "508b2e8000078773c7884e17b8d1ca711f4313ba14ff0000f7a28af68b02e71a" }, "monterey": { "cellar": ":any_skip_relocation", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:c13826f376fd6dbf5afb4d9a435f39abb10b795473c8f25b9db986c731d627ee", - "sha256": "c13826f376fd6dbf5afb4d9a435f39abb10b795473c8f25b9db986c731d627ee" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:8c367a76ca05ba07ffc38a6bb2f5ee0231d363655ed982284afcb4d685fa524e", + "sha256": "8c367a76ca05ba07ffc38a6bb2f5ee0231d363655ed982284afcb4d685fa524e" }, "x86_64_linux": { "cellar": "/home/linuxbrew/.linuxbrew/Cellar", - "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:c9400f4f4641d817facf5621eed3d2fe078361c83c80d34b91763185068ea740", - "sha256": "c9400f4f4641d817facf5621eed3d2fe078361c83c80d34b91763185068ea740" + "url": "https://ghcr.io/v2/homebrew/core/swiftformat/blobs/sha256:8e8abc969e1b10e7411a92717703c8cd944c7a3f3fd04e2d232918971ae14662", + "sha256": "8e8abc969e1b10e7411a92717703c8cd944c7a3f3fd04e2d232918971ae14662" } } } @@ -82,12 +87,12 @@ "macOS": "13.6" }, "sonoma": { - "HOMEBREW_VERSION": "4.2.2-18-gdcd267b", + "HOMEBREW_VERSION": "4.3.24", "HOMEBREW_PREFIX": "/opt/homebrew", "Homebrew/homebrew-core": "api", - "CLT": "15.0.0.0.1.1694021235", - "Xcode": "15.1", - "macOS": "14.2.1" + "CLT": "16.0.0.0.1.1724870825", + "Xcode": "15.4", + "macOS": "14.6.1" } } } diff --git a/fastlane/swift/formatting/Rakefile b/fastlane/swift/formatting/Rakefile index ad91133d3e9..1a7f701fb8f 100644 --- a/fastlane/swift/formatting/Rakefile +++ b/fastlane/swift/formatting/Rakefile @@ -8,6 +8,7 @@ task(:brew) do raise '`brew` is required. Please install brew. https://brew.sh/' unless system('which brew') puts('➡️ Brew') + sh('brew update') sh('brew bundle') end diff --git a/fastlane_core/lib/fastlane_core/build_watcher.rb b/fastlane_core/lib/fastlane_core/build_watcher.rb index 843b3edd6f0..062a55a3782 100644 --- a/fastlane_core/lib/fastlane_core/build_watcher.rb +++ b/fastlane_core/lib/fastlane_core/build_watcher.rb @@ -116,6 +116,11 @@ def matching_build(watched_app_version: nil, watched_build_version: nil, app_id: # Raise error if more than 1 build is returned # This should never happen but need to inform the user if it does matched_builds = version_matches.map(&:builds).flatten + + # Need to filter out duplicate builds (which could be a result from the double X.Y.0 and X.Y queries) + # See: https://github.com/fastlane/fastlane/issues/22248 + matched_builds = matched_builds.uniq(&:id) + if matched_builds.size > 1 && !select_latest error_builds = matched_builds.map do |build| "#{build.app_version}(#{build.version}) for #{build.platform} - #{build.processing_state}" diff --git a/fastlane_core/lib/fastlane_core/cert_checker.rb b/fastlane_core/lib/fastlane_core/cert_checker.rb index ddc29baa5f2..f6c8021f696 100644 --- a/fastlane_core/lib/fastlane_core/cert_checker.rb +++ b/fastlane_core/lib/fastlane_core/cert_checker.rb @@ -39,6 +39,7 @@ class CertChecker def self.installed?(path, in_keychain: nil) UI.user_error!("Could not find file '#{path}'") unless File.exist?(path) + in_keychain &&= FastlaneCore::Helper.keychain_path(in_keychain) ids = installed_identities(in_keychain: in_keychain) ids += installed_installers(in_keychain: in_keychain) finger_print = sha1_fingerprint(path) @@ -52,7 +53,7 @@ def self.is_installed?(path) end def self.installed_identities(in_keychain: nil) - install_missing_wwdr_certificates + install_missing_wwdr_certificates(in_keychain: in_keychain) available = list_available_identities(in_keychain: in_keychain) # Match for this text against word boundaries to avoid edge cases around multiples of 10 identities! @@ -111,12 +112,13 @@ def self.list_available_developer_id_installer(in_keychain: nil) `#{commands.join(' ')}` end - def self.installed_wwdr_certificates + def self.installed_wwdr_certificates(keychain: nil) certificate_name = "Apple Worldwide Developer Relations" + keychain ||= wwdr_keychain # backwards compatibility # Find all installed WWDRCA certificates installed_certs = [] - Helper.backticks("security find-certificate -a -c '#{certificate_name}' -p #{wwdr_keychain.shellescape}", print: false) + Helper.backticks("security find-certificate -a -c '#{certificate_name}' -p #{keychain.shellescape}", print: false) .lines .each do |line| if line.start_with?('-----BEGIN CERTIFICATE-----') @@ -135,20 +137,21 @@ def self.installed_wwdr_certificates .compact end - def self.install_missing_wwdr_certificates + def self.install_missing_wwdr_certificates(in_keychain: nil) # Install all Worldwide Developer Relations Intermediate Certificates listed here: https://www.apple.com/certificateauthority/ - missing = WWDRCA_CERTIFICATES.map { |c| c[:alias] } - installed_wwdr_certificates + keychain = in_keychain || wwdr_keychain + missing = WWDRCA_CERTIFICATES.map { |c| c[:alias] } - installed_wwdr_certificates(keychain: keychain) missing.each do |cert_alias| - install_wwdr_certificate(cert_alias) + install_wwdr_certificate(cert_alias, keychain: keychain) end missing.count end - def self.install_wwdr_certificate(cert_alias) + def self.install_wwdr_certificate(cert_alias, keychain: nil) url = WWDRCA_CERTIFICATES.find { |c| c[:alias] == cert_alias }.fetch(:url) file = Tempfile.new([File.basename(url, ".cer"), ".cer"]) filename = file.path - keychain = wwdr_keychain + keychain ||= wwdr_keychain # backwards compatibility keychain = "-k #{keychain.shellescape}" unless keychain.empty? # Attempts to fix an issue installing WWDR cert tends to fail on CIs diff --git a/fastlane_core/lib/fastlane_core/device_manager.rb b/fastlane_core/lib/fastlane_core/device_manager.rb index 0ac4128f02c..b485bd5e69a 100644 --- a/fastlane_core/lib/fastlane_core/device_manager.rb +++ b/fastlane_core/lib/fastlane_core/device_manager.rb @@ -13,7 +13,7 @@ def all(requested_os_type = "") def runtime_build_os_versions @runtime_build_os_versions ||= begin - output, status = Open3.capture2('xcrun simctl list runtimes -j') + output, status = Open3.capture2('xcrun simctl list -j runtimes') raise status unless status.success? json = JSON.parse(output) json['runtimes'].map { |h| [h['buildversion'], h['version']] }.to_h diff --git a/fastlane_core/lib/fastlane_core/helper.rb b/fastlane_core/lib/fastlane_core/helper.rb index e73dd1b1fd8..05ee4f284c4 100644 --- a/fastlane_core/lib/fastlane_core/helper.rb +++ b/fastlane_core/lib/fastlane_core/helper.rb @@ -482,20 +482,5 @@ def self.ask_password(message: "Passphrase: ", confirm: nil, confirmation_messag UI.error("Your entries do not match. Please try again") end end - - # URI.open added by `require 'open-uri'` is not available in Ruby 2.4. This helper lets you open a URI - # by choosing appropriate interface to do so depending on Ruby version. This helper is subject to be removed - # when fastlane drops Ruby 2.4 support. - def self.open_uri(*rest, &block) - require 'open-uri' - - if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.5') - dup = rest.dup - uri = dup.shift - URI.parse(uri).open(*dup, &block) - else - URI.open(*rest, &block) - end - end end end diff --git a/fastlane_core/lib/fastlane_core/print_table.rb b/fastlane_core/lib/fastlane_core/print_table.rb index 622350d2028..3abc97ea244 100644 --- a/fastlane_core/lib/fastlane_core/print_table.rb +++ b/fastlane_core/lib/fastlane_core/print_table.rb @@ -1,6 +1,22 @@ require_relative 'configuration/configuration' require_relative 'helper' +# Monkey patch Terminal::Table until this is merged +# https://github.com/tj/terminal-table/pull/131 +# solves https://github.com/fastlane/fastlane/issues/21852 +# loads Terminal::Table first to be able to monkey patch it. +require 'terminal-table' +module Terminal + class Table + class Cell + def lines + # @value.to_s.split(/\n/) + @value.to_s.encode("utf-8", invalid: :replace).split(/\n/) + end + end + end +end + module FastlaneCore class PrintTable class << self diff --git a/fastlane_core/lib/fastlane_core/project.rb b/fastlane_core/lib/fastlane_core/project.rb index 8ae9b759419..b72a919ffa6 100644 --- a/fastlane_core/lib/fastlane_core/project.rb +++ b/fastlane_core/lib/fastlane_core/project.rb @@ -307,6 +307,10 @@ def watchos? supported_platforms.include?(:watchOS) end + def visionos? + supported_platforms.include?(:visionOS) + end + def multiplatform? supported_platforms.count > 1 end @@ -323,6 +327,7 @@ def supported_platforms when "iphonesimulator", "iphoneos" then :iOS when "watchsimulator", "watchos" then :watchOS when "appletvsimulator", "appletvos" then :tvOS + when "xros", "xrsimulator" then :visionOS end end.uniq.compact end @@ -336,6 +341,7 @@ def xcodebuild_parameters proj << "-derivedDataPath #{options[:derived_data_path].shellescape}" if options[:derived_data_path] proj << "-xcconfig #{options[:xcconfig].shellescape}" if options[:xcconfig] proj << "-scmProvider system" if options[:use_system_scm] + proj << "-packageAuthorizationProvider #{options[:package_authorization_provider].shellescape}" if options[:package_authorization_provider] xcode_at_least_11 = FastlaneCore::Helper.xcode_at_least?('11.0') if xcode_at_least_11 && options[:cloned_source_packages_path] diff --git a/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb b/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb index db84fb6a795..a905393acdf 100644 --- a/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb +++ b/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb @@ -243,10 +243,6 @@ def suggest_ruby_reinstall(e) ui.error(e.to_s) ui.error("") ui.error("SSL errors can be caused by various components on your local machine.") - if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.1') - ui.error("Apple has recently changed their servers to require TLS 1.2, which may") - ui.error("not be available to your system installed Ruby (#{RUBY_VERSION})") - end ui.error("") ui.error("The best solution is to use the self-contained fastlane version.") ui.error("Which ships with a bundled OpenSSL,ruby and all gems - so you don't depend on system libraries") diff --git a/fastlane_core/lib/fastlane_core/ui/help_formatter.rb b/fastlane_core/lib/fastlane_core/ui/help_formatter.rb index bf9a546b586..9ce3b58aa15 100644 --- a/fastlane_core/lib/fastlane_core/ui/help_formatter.rb +++ b/fastlane_core/lib/fastlane_core/ui/help_formatter.rb @@ -6,11 +6,7 @@ def template(name) # fastlane only customizes the global command help return super unless name == :help - if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6') - ERB.new(File.read(File.join(File.dirname(__FILE__), "help.erb")), nil, '-') - else - ERB.new(File.read(File.join(File.dirname(__FILE__), "help.erb")), trim_mode: '-') - end + ERB.new(File.read(File.join(File.dirname(__FILE__), "help.erb")), trim_mode: '-') end end end diff --git a/fastlane_core/lib/fastlane_core/ui/implementations/shell.rb b/fastlane_core/lib/fastlane_core/ui/implementations/shell.rb index 87f448a06ea..19fa1e48671 100644 --- a/fastlane_core/lib/fastlane_core/ui/implementations/shell.rb +++ b/fastlane_core/lib/fastlane_core/ui/implementations/shell.rb @@ -108,12 +108,14 @@ def content_error(content, error_line) start_line = error_line - 2 < 1 ? 1 : error_line - 2 end_line = error_line + 2 < contents.length ? error_line + 2 : contents.length + error('```') Range.new(start_line, end_line).each do |line| str = line == error_line ? " => " : " " str << line.to_s.rjust(Math.log10(end_line) + 1) str << ":\t#{contents[line - 1]}" error(str) end + error('```') end ##################################################### diff --git a/fastlane_core/spec/build_watcher_spec.rb b/fastlane_core/spec/build_watcher_spec.rb index 3a28444a49a..e7204802a8c 100644 --- a/fastlane_core/spec/build_watcher_spec.rb +++ b/fastlane_core/spec/build_watcher_spec.rb @@ -13,6 +13,7 @@ end let(:ready_build) do double( + id: '123', app_version: "1.0", version: "1", processed?: true, @@ -20,6 +21,16 @@ processing_state: 'VALID' ) end + let(:ready_build_dup) do + double( + id: '321', + app_version: "1.0.0", + version: "1", + processed?: true, + platform: 'IOS', + processing_state: 'VALID' + ) + end let(:mock_base_api_client) { "fake api base client" } @@ -133,7 +144,7 @@ describe 'multiple builds found' do describe 'select_latest is false' do it 'raises error select_latest is false' do - builds = [ready_build, ready_build] + builds = [ready_build, ready_build_dup] expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0).and_return([]) expect(Spaceship::ConnectAPI::Build).to receive(:all).with(options_1_0_0).and_return([]) @@ -155,6 +166,7 @@ describe 'select_latest is true' do let(:newest_ready_build) do double( + id: "853", app_version: "1.0", version: "2", processed?: true, @@ -479,6 +491,7 @@ let(:newest_ready_build) do double( + id: '482', app_version: "1.0", version: "2", processed?: true, diff --git a/fastlane_core/spec/cert_checker_spec.rb b/fastlane_core/spec/cert_checker_spec.rb index 1201ca6b633..e0d4b1311d7 100644 --- a/fastlane_core/spec/cert_checker_spec.rb +++ b/fastlane_core/spec/cert_checker_spec.rb @@ -11,6 +11,7 @@ class ProcessStatusMock describe '#installed_identities' do it 'should print an error when no local code signing identities are found' do + allow(FastlaneCore::CertChecker).to receive(:wwdr_keychain).and_return('login.keychain') allow(FastlaneCore::CertChecker).to receive(:installed_wwdr_certificates).and_return(['G2', 'G3', 'G4', 'G5', 'G6']) allow(FastlaneCore::CertChecker).to receive(:list_available_identities).and_return(" 0 valid identities found\n") expect(FastlaneCore::UI).to receive(:error).with(/There are no local code signing identities found/) @@ -19,6 +20,7 @@ class ProcessStatusMock end it 'should not be fooled by 10 local code signing identities available' do + allow(FastlaneCore::CertChecker).to receive(:wwdr_keychain).and_return('login.keychain') allow(FastlaneCore::CertChecker).to receive(:installed_wwdr_certificates).and_return(['G2', 'G3', 'G4', 'G5', 'G6']) allow(FastlaneCore::CertChecker).to receive(:list_available_identities).and_return(" 10 valid identities found\n") expect(FastlaneCore::UI).not_to(receive(:error)) @@ -61,18 +63,20 @@ class ProcessStatusMock describe '#install_missing_wwdr_certificates' do it 'should install all official WWDR certificates' do + allow(FastlaneCore::CertChecker).to receive(:wwdr_keychain).and_return('login.keychain') allow(FastlaneCore::CertChecker).to receive(:installed_wwdr_certificates).and_return([]) - expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G2') - expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G3') - expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G4') - expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G5') - expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G6') + expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G2', { keychain: "login.keychain" }) + expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G3', { keychain: "login.keychain" }) + expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G4', { keychain: "login.keychain" }) + expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G5', { keychain: "login.keychain" }) + expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G6', { keychain: "login.keychain" }) FastlaneCore::CertChecker.install_missing_wwdr_certificates end it 'should install the missing official WWDR certificate' do + allow(FastlaneCore::CertChecker).to receive(:wwdr_keychain).and_return('login.keychain') allow(FastlaneCore::CertChecker).to receive(:installed_wwdr_certificates).and_return(['G2', 'G3', 'G4', 'G5']) - expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G6') + expect(FastlaneCore::CertChecker).to receive(:install_wwdr_certificate).with('G6', { keychain: "login.keychain" }) FastlaneCore::CertChecker.install_missing_wwdr_certificates end diff --git a/fastlane_core/spec/device_manager_spec.rb b/fastlane_core/spec/device_manager_spec.rb index a4b76ae4d47..f34b5dd1b10 100644 --- a/fastlane_core/spec/device_manager_spec.rb +++ b/fastlane_core/spec/device_manager_spec.rb @@ -23,7 +23,7 @@ it 'raises an error if broken xcrun simctl list runtimes' do status = double('status', "success?": true) - expect(Open3).to receive(:capture2).with("xcrun simctl list runtimes -j").and_return(['garbage', status]) + expect(Open3).to receive(:capture2).with("xcrun simctl list -j runtimes").and_return(['garbage', status]) expect do FastlaneCore::DeviceManager.runtime_build_os_versions @@ -439,7 +439,7 @@ it 'properly parses `xcrun simctl list runtimes` to associate runtime builds with their exact OS version' do status = double('status', "success?": true) runtime_output = File.read('./fastlane_core/spec/fixtures/XcrunSimctlListRuntimesOutput') - expect(Open3).to receive(:capture2).with("xcrun simctl list runtimes -j").and_return([runtime_output, status]) + expect(Open3).to receive(:capture2).with("xcrun simctl list -j runtimes").and_return([runtime_output, status]) expect(FastlaneCore::DeviceManager.runtime_build_os_versions['21A328']).to eq('17.0') expect(FastlaneCore::DeviceManager.runtime_build_os_versions['21A342']).to eq('17.0.1') diff --git a/fastlane_core/spec/helper_spec.rb b/fastlane_core/spec/helper_spec.rb index 66b36008cdb..6b1964f8cd4 100644 --- a/fastlane_core/spec/helper_spec.rb +++ b/fastlane_core/spec/helper_spec.rb @@ -230,37 +230,5 @@ expect(FastlaneCore::Helper.fastlane_enabled?).to be(true) end end - - describe '#open_uri' do - before do - stub_request(:get, 'https://fastlane.tools').to_return(body: 'SOME_TEXT', status: 200) - end - - it 'performs URI.open and return IO like object that can be read' do - expect(FastlaneCore::Helper.open_uri('https://fastlane.tools')).to respond_to(:read) - end - - it 'performs URI.open with block' do - is_block_called = false - FastlaneCore::Helper.open_uri('https://fastlane.tools') do |content| - expect(content).to respond_to(:read) - is_block_called = true - end - expect(is_block_called).to be(true) - end - - it 'performs URI.open with options' do - expect(FastlaneCore::Helper.open_uri('https://fastlane.tools', 'rb')).to respond_to(:read) - end - - it 'performs URI.open with options and block' do - is_block_called = false - FastlaneCore::Helper.open_uri('https://fastlane.tools', 'rb') do |content| - expect(content).to respond_to(:read) - is_block_called = true - end - expect(is_block_called).to be(true) - end - end end end diff --git a/fastlane_core/spec/project_spec.rb b/fastlane_core/spec/project_spec.rb index 47662bc3ec1..37511fb51c7 100644 --- a/fastlane_core/spec/project_spec.rb +++ b/fastlane_core/spec/project_spec.rb @@ -508,6 +508,17 @@ def count_processes(text) end end + describe "xcodebuild package_authorization_provider" do + it 'generates an xcodebuild -showBuildSettings command that includes package_authorization_provider if provided in options', requires_xcode: true do + project = FastlaneCore::Project.new({ + project: "./fastlane_core/spec/fixtures/projects/Example.xcodeproj", + package_authorization_provider: "keychain" + }) + command = "xcodebuild -showBuildSettings -project ./fastlane_core/spec/fixtures/projects/Example.xcodeproj -packageAuthorizationProvider keychain 2>&1" + expect(project.build_xcodebuild_showbuildsettings_command).to eq(command) + end + end + describe 'xcodebuild_xcconfig option', requires_xcode: true do it 'generates an xcodebuild -showBuildSettings command without xcconfig by default' do project = FastlaneCore::Project.new({ project: "./fastlane_core/spec/fixtures/projects/Example.xcodeproj" }) diff --git a/frameit/frames_generator/Rakefile b/frameit/frames_generator/Rakefile index bb0ce071617..45172055f6b 100644 --- a/frameit/frames_generator/Rakefile +++ b/frameit/frames_generator/Rakefile @@ -88,7 +88,7 @@ task(:generate_device_frames) do puts("Download and extract the latest devices from Facebook") puts("") - puts(" https://facebook.design/devices") + puts(" https://design.facebook.com/toolsandresources/devices/") puts("") confirm puts("Currently the script renames iPad files according to the following scheme:") @@ -176,12 +176,20 @@ end # Facebook's device naming is inconsistent. This method fixes the file names. def sanitize_filename(filename) - perform_ipad_renaming(filename - .gsub('Grey', 'Gray') # some Apple devices have "Grey" instead of "Gray" color -> unify - .gsub(' - Portrait', '') # iPads Pro include Portrait and Landscape - we just need Portrait; Landscape filtered via DEVICES_TO_SKIP - .gsub(' - ', ' ') # Google Pixel device names are separated from their colors by a dash -> remove - .gsub('Note10', 'Note 10') # Samsung Galaxy Note 10 is missing a space in "Note10" - .gsub('Mi Mix Alpha Front', 'Mi Mix Alpha')) # Xiaomi Mi Mix Alpha contains the words "Front", "Back" and "Side" => back and side are filtered via DEVICES_TO_SKIP, "Front" removed from the name here + renamed = filename + .gsub('Grey', 'Gray') # some Apple devices have "Grey" instead of "Gray" color -> unify + .gsub('Golden', 'Gold') # some Apple devices have "Golden" instead of "Gold" color -> unify + .gsub('(PRODUCT)Red', 'Red') # some Apple devices have "(Product)Red" instead of "Red" -> unify + .gsub('(PRODUCT)RED', 'Red') # some Apple devices have "(Product)Red" instead of "Red" -> unify + .gsub('Space Black', 'Black') # "iPhone 14 Pro and Pro Max use "Space Black" instead of "Black" -> unify + .gsub('Deep Purple', 'Purple') # "iPhone 14 Pro and Pro Max use "Deep Purple" instead of "Purple" -> unify + .gsub(' - Portrait', '') # iPads Pro include Portrait and Landscape - we just need Portrait; Landscape filtered via DEVICES_TO_SKIP + .gsub(' - ', ' ') # Google Pixel device names are separated from their colors by a dash -> remove + .gsub(' – ', ' ') # some Apple devices are separated from their colors by an en dash -> remove + .gsub(' — ', ' ') # some Apple devices are separated from their colors by an em dash -> remove + .gsub('Note10', 'Note 10') # Samsung Galaxy Note 10 is missing a space in "Note10" + .gsub('Mi Mix Alpha Front', 'Mi Mix Alpha') + perform_ipad_renaming(renamed) # Xiaomi Mi Mix Alpha contains the words "Front", "Back" and "Side" => back and side are filtered via DEVICES_TO_SKIP, "Front" removed from the name here end # Facebook doesn't include versions in iPad files - this function makes sure the correct versions are added @@ -262,9 +270,13 @@ def measure_slot(path) end def sanitize_device_name(filename) - basename = File.basename(filename, File.extname(filename)) - basename = basename.gsub("Apple", "") - basename = basename.gsub("-", " ") + basename = File + .basename(filename, File.extname(filename)) + .gsub("Apple", "") + .gsub("-", " ") + .gsub(' - ', ' ') # Google Pixel device names are separated from their colors by a dash -> remove + .gsub(' – ', ' ') # some Apple devices are separated from their colors by this weird dash -> remove + .gsub(' — ', ' ') # some Apple devices are separated from their colors by this weird dash -> remove # Directory is named "Nexus 5X" but file named "Nexus 5x" # Need to rename to "Nexus 5X" diff --git a/frameit/lib/frameit/device_types.rb b/frameit/lib/frameit/device_types.rb index 088da1725fd..1bdb355d20c 100644 --- a/frameit/lib/frameit/device_types.rb +++ b/frameit/lib/frameit/device_types.rb @@ -133,6 +133,10 @@ module Devices IPHONE_13_PRO ||= Frameit::Device.new("iphone-13-pro", "Apple iPhone 13 Pro", 11, [[1170, 2532], [2532, 1170]], 460, Color::GRAPHITE, Platform::IOS) IPHONE_13_PRO_MAX ||= Frameit::Device.new("iphone13-pro-max", "Apple iPhone 13 Pro Max", 11, [[1284, 2778], [2778, 1284]], 458, Color::GRAPHITE, Platform::IOS) IPHONE_13_MINI ||= Frameit::Device.new("iphone-13-mini", "Apple iPhone 13 Mini", 11, [[1080, 2340], [2340, 1080]], 476, Color::MIDNIGHT, Platform::IOS) + IPHONE_14 ||= Frameit::Device.new("iphone-14", "Apple iPhone 14", 12, [[1170, 2532], [2532, 1170]], 460, Color::MIDNIGHT, Platform::IOS) + IPHONE_14_PLUS ||= Frameit::Device.new("iphone-14-plus", "Apple iPhone 14 Plus", 12, [[1284, 2778], [2778, 1284]], 458, Color::MIDNIGHT, Platform::IOS) + IPHONE_14_PRO ||= Frameit::Device.new("iphone-14-pro", "Apple iPhone 14 Pro", 12, [[1178, 2556], [2556, 1178]], 460, Color::PURPLE, Platform::IOS) + IPHONE_14_PRO_MAX ||= Frameit::Device.new("iphone14-pro-max", "Apple iPhone 14 Pro Max", 12, [[1290, 2796], [2796, 1290]], 458, Color::PURPLE, Platform::IOS) IPAD_10_2 ||= Frameit::Device.new("ipad-10-2", "Apple iPad 10.2", 1, [[1620, 2160], [2160, 1620]], 264, Color::SPACE_GRAY, Platform::IOS) IPAD_AIR_2 ||= Frameit::Device.new("ipad-air-2", "Apple iPad Air 2", 1, [[1536, 2048], [2048, 1536]], 264, Color::SPACE_GRAY, Platform::IOS, Deliver::AppScreenshot::ScreenSize::IOS_IPAD) IPAD_AIR_2019 ||= Frameit::Device.new("ipad-air-2019", "Apple iPad Air (2019)", 2, [[1668, 2224], [2224, 1668]], 265, Color::SPACE_GRAY, Platform::IOS) diff --git a/frameit/lib/frameit/editor.rb b/frameit/lib/frameit/editor.rb index 718297f6564..55e98a9e4c4 100644 --- a/frameit/lib/frameit/editor.rb +++ b/frameit/lib/frameit/editor.rb @@ -109,6 +109,26 @@ def put_into_frame end end + # Apply rounded corners for all iPhone 14 devices + if screenshot.device.id.to_s.include?("iphone-14") || screenshot.device.id.to_s.include?("iphone14") + + maskData = MiniMagick::Tool::Convert.new do |img| + img.size("#{screenshot.size[0]}x#{screenshot.size[1]}") + img.canvas('none') + img.draw("roundrectangle 0,0,#{screenshot.size[0]},#{screenshot.size[1]},100,100") + img << 'png:-' + end + + # Create a mask + mask = MiniMagick::Image.read(maskData) + + @image = @image.composite(mask, "png") do |c| + c.channel("A") + c.compose("DstIn") + c.alpha("on") + end + end + @image = frame.composite(image, "png") do |c| c.compose("DstOver") c.geometry(offset['offset']) diff --git a/frameit/spec/device_spec.rb b/frameit/spec/device_spec.rb index 24d455d2621..9f000620205 100644 --- a/frameit/spec/device_spec.rb +++ b/frameit/spec/device_spec.rb @@ -33,9 +33,9 @@ def expect_forced_screen_size(value) expect_screen_size_from_file("Apple iPhone XS-Landscape{2436x1125}.jpg", Platform::IOS).to eq(Devices::IPHONE_XS) end - it "should detect iPhone 13 in portrait and landscape based on priority" do - expect_screen_size_from_file("screenshot-Portrait{1170x2532}.jpg", Platform::IOS).to eq(Devices::IPHONE_13_PRO) - expect_screen_size_from_file("screenshot-Landscape{2532x1170}.jpg", Platform::IOS).to eq(Devices::IPHONE_13_PRO) + it "should detect iPhone 14 in portrait and landscape based on priority" do + expect_screen_size_from_file("screenshot-Portrait{1170x2532}.jpg", Platform::IOS).to eq(Devices::IPHONE_14) + expect_screen_size_from_file("screenshot-Landscape{2532x1170}.jpg", Platform::IOS).to eq(Devices::IPHONE_14) end it "should detect iPhone X instead of iPhone XS because of the file name containing device name" do diff --git a/gym/examples/visionos/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json b/gym/examples/visionos/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json new file mode 100644 index 00000000000..4a8c74bf3cb --- /dev/null +++ b/gym/examples/visionos/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json @@ -0,0 +1,11 @@ +{ + "pathsToIds" : { + "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/GridMaterial.usda" : "440DE5B4-E4E4-459B-AABF-9ACE96319272", + "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/procedural_sphere_grid.usda" : "34C460AE-CA1B-4348-BD05-621ACBDFFE97", + "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Scene.usda" : "0A9B4653-B11E-4D6A-850E-C6FCB621626C", + "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Untitled Scene.usda" : "03E02005-EFA6-48D6-8A76-05B2822A74E9", + "RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/GridMaterial.usda" : "FBD8436F-6B8B-4B82-99B5-995D538B4704", + "RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/procedural_sphere_grid.usda" : "1CBF3893-ABFD-408C-8B91-045BFD257808", + "RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Scene.usda" : "26DBAE76-5DD8-47B6-A085-1B4ADA111097" + } +} \ No newline at end of file diff --git a/gym/examples/visionos/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/SceneMetadataList.json b/gym/examples/visionos/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/SceneMetadataList.json new file mode 100644 index 00000000000..1d84a750e4b --- /dev/null +++ b/gym/examples/visionos/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/SceneMetadataList.json @@ -0,0 +1,209 @@ +{ + "0A9B4653-B11E-4D6A-850E-C6FCB621626C" : { + "cameraTransform" : [ + 0.9807314, + -1.9820146e-10, + -0.195361, + 0, + -0.10051192, + 0.85749435, + -0.5045798, + 0, + 0.16752096, + 0.51449335, + 0.84097165, + 0, + 0.09084191, + 0.05849296, + 0.13903293, + 1 + ], + "objectMetadataList" : [ + [ + "0A9B4653-B11E-4D6A-850E-C6FCB621626C", + "Root" + ], + { + "isExpanded" : true, + "isLocked" : false + }, + [ + "0A9B4653-B11E-4D6A-850E-C6FCB621626C", + "Root", + "GridMaterial" + ], + { + "isExpanded" : true, + "isLocked" : false + }, + [ + "0A9B4653-B11E-4D6A-850E-C6FCB621626C", + "Root", + "Sphere" + ], + { + "isExpanded" : true, + "isLocked" : false + } + ] + }, + "1CBF3893-ABFD-408C-8B91-045BFD257808" : { + "cameraTransform" : [ + 0.99999994, + 0, + -0, + 0, + -0, + 0.8660255, + -0.49999988, + 0, + 0, + 0.49999988, + 0.8660255, + 0, + 0, + 0.27093542, + 0.46927398, + 1 + ], + "objectMetadataList" : [ + + ] + }, + "03E02005-EFA6-48D6-8A76-05B2822A74E9" : { + "cameraTransform" : [ + 0.99999994, + 0, + -0, + 0, + -0, + 0.8660254, + -0.49999994, + 0, + 0, + 0.49999994, + 0.8660254, + 0, + 0, + 0.5981957, + 1.0361054, + 1 + ], + "objectMetadataList" : [ + + ] + }, + "26DBAE76-5DD8-47B6-A085-1B4ADA111097" : { + "cameraTransform" : [ + 1, + 0, + -0, + 0, + -0, + 0.7071069, + -0.7071067, + 0, + 0, + 0.7071067, + 0.7071069, + 0, + 0, + 0.2681068, + 0.26850593, + 1 + ], + "objectMetadataList" : [ + [ + "26DBAE76-5DD8-47B6-A085-1B4ADA111097", + "Root" + ], + { + "isExpanded" : true, + "isLocked" : false + } + ] + }, + "34C460AE-CA1B-4348-BD05-621ACBDFFE97" : { + "cameraTransform" : [ + 0.99999994, + 0, + -0, + 0, + -0, + 0.8660255, + -0.49999988, + 0, + 0, + 0.49999988, + 0.8660255, + 0, + 0, + 0.27093542, + 0.46927398, + 1 + ], + "objectMetadataList" : [ + + ] + }, + "440DE5B4-E4E4-459B-AABF-9ACE96319272" : { + "cameraTransform" : [ + 0.99999994, + 0, + -0, + 0, + -0, + 0.8660254, + -0.49999994, + 0, + 0, + 0.49999994, + 0.8660254, + 0, + 0, + 0.5981957, + 1.0361054, + 1 + ], + "objectMetadataList" : [ + [ + "440DE5B4-E4E4-459B-AABF-9ACE96319272", + "Root" + ], + { + "isExpanded" : true, + "isLocked" : false + } + ] + }, + "FBD8436F-6B8B-4B82-99B5-995D538B4704" : { + "cameraTransform" : [ + 0.99999994, + 0, + -0, + 0, + -0, + 0.8660254, + -0.49999994, + 0, + 0, + 0.49999994, + 0.8660254, + 0, + 0, + 0.5981957, + 1.0361054, + 1 + ], + "objectMetadataList" : [ + [ + "FBD8436F-6B8B-4B82-99B5-995D538B4704", + "Root" + ], + { + "isExpanded" : true, + "isLocked" : false + } + ] + } +} \ No newline at end of file diff --git a/gym/examples/visionos/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/Settings.rcprojectdata b/gym/examples/visionos/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/Settings.rcprojectdata new file mode 100644 index 00000000000..6dea95c8d32 --- /dev/null +++ b/gym/examples/visionos/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/Settings.rcprojectdata @@ -0,0 +1,17 @@ +{ + "cameraPresets" : { + + }, + "secondaryToolbarData" : { + "isGridVisible" : true, + "sceneReverbPreset" : -1 + }, + "unitDefaults" : { + "°" : "°", + "kg" : "g", + "m" : "cm", + "m\/s" : "m\/s", + "m\/s²" : "m\/s²", + "s" : "s" + } +} \ No newline at end of file diff --git a/gym/examples/visionos/Packages/RealityKitContent/Package.swift b/gym/examples/visionos/Packages/RealityKitContent/Package.swift new file mode 100644 index 00000000000..d043ae1ad56 --- /dev/null +++ b/gym/examples/visionos/Packages/RealityKitContent/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "RealityKitContent", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "RealityKitContent", + targets: ["RealityKitContent"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "RealityKitContent", + dependencies: []), + ] +) \ No newline at end of file diff --git a/gym/examples/visionos/Packages/RealityKitContent/README.md b/gym/examples/visionos/Packages/RealityKitContent/README.md new file mode 100644 index 00000000000..d82e3d057a3 --- /dev/null +++ b/gym/examples/visionos/Packages/RealityKitContent/README.md @@ -0,0 +1,3 @@ +# RealityKitContent + +A visionOS demo project used to run integration test in `gym/spec/platform_detection_spec.rb` (and maybe in the future in other tests too). \ No newline at end of file diff --git a/gym/examples/visionos/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Materials/GridMaterial.usda b/gym/examples/visionos/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Materials/GridMaterial.usda new file mode 100644 index 00000000000..b7afd0240f5 --- /dev/null +++ b/gym/examples/visionos/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Materials/GridMaterial.usda @@ -0,0 +1,216 @@ +#usda 1.0 +( + defaultPrim = "Root" + metersPerUnit = 1 + upAxis = "Y" +) + +def Xform "Root" +{ + def Material "GridMaterial" + { + reorder nameChildren = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "DefaultSurfaceShader", "MaterialXPreviewSurface", "Texcoord", "Add", "Multiply", "Fractional", "LineCounts", "Multiply_1", "Separate2", "Separate2_1", "Ifgreater", "Ifgreater_1", "Max", "Background_Color"] + token outputs:mtlx:surface.connect = + token outputs:realitykit:vertex + token outputs:surface + float2 ui:nodegraph:realitykit:subgraphOutputs:pos = (2222, 300.5) + float2 ui:nodegraph:realitykit:subgraphOutputs:size = (182, 89) + int ui:nodegraph:realitykit:subgraphOutputs:stackingOrder = 749 + + def Shader "DefaultSurfaceShader" + { + uniform token info:id = "UsdPreviewSurface" + color3f inputs:diffuseColor = (1, 1, 1) + float inputs:roughness = 0.75 + token outputs:surface + } + + def Shader "MaterialXPreviewSurface" + { + uniform token info:id = "ND_UsdPreviewSurface_surfaceshader" + float inputs:clearcoat + float inputs:clearcoatRoughness + color3f inputs:diffuseColor.connect = + color3f inputs:emissiveColor + float inputs:ior + float inputs:metallic = 0.15 + float3 inputs:normal + float inputs:occlusion + float inputs:opacity + float inputs:opacityThreshold + float inputs:roughness = 0.5 + token outputs:out + float2 ui:nodegraph:node:pos = (1967, 300.5) + float2 ui:nodegraph:node:size = (208, 297) + int ui:nodegraph:node:stackingOrder = 870 + string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["Advanced"] + } + + def Shader "Texcoord" + { + uniform token info:id = "ND_texcoord_vector2" + float2 outputs:out + float2 ui:nodegraph:node:pos = (94.14453, 35.29297) + float2 ui:nodegraph:node:size = (182, 43) + int ui:nodegraph:node:stackingOrder = 1358 + } + + def Shader "Multiply" + { + uniform token info:id = "ND_multiply_vector2" + float2 inputs:in1.connect = + float2 inputs:in2 = (32, 15) + float2 inputs:in2.connect = + float2 outputs:out + float2 ui:nodegraph:node:pos = (275.64453, 47.29297) + float2 ui:nodegraph:node:size = (61, 36) + int ui:nodegraph:node:stackingOrder = 1348 + string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["inputs:in2"] + } + + def Shader "Fractional" + { + uniform token info:id = "ND_realitykit_fractional_vector2" + float2 inputs:in.connect = + float2 outputs:out + float2 ui:nodegraph:node:pos = (440.5, 49.5) + float2 ui:nodegraph:node:size = (155, 99) + int ui:nodegraph:node:stackingOrder = 1345 + } + + def Shader "BaseColor" + { + uniform token info:id = "ND_constant_color3" + color3f inputs:value = (0.89737034, 0.89737034, 0.89737034) ( + colorSpace = "Input - Texture - sRGB - sRGB" + ) + color3f inputs:value.connect = None + color3f outputs:out + float2 ui:nodegraph:node:pos = (1537.5977, 363.07812) + float2 ui:nodegraph:node:size = (150, 43) + int ui:nodegraph:node:stackingOrder = 1353 + } + + def Shader "LineColor" + { + uniform token info:id = "ND_constant_color3" + color3f inputs:value = (0.55945957, 0.55945957, 0.55945957) ( + colorSpace = "Input - Texture - sRGB - sRGB" + ) + color3f inputs:value.connect = None + color3f outputs:out + float2 ui:nodegraph:node:pos = (1536.9844, 287.86328) + float2 ui:nodegraph:node:size = (146, 43) + int ui:nodegraph:node:stackingOrder = 1355 + } + + def Shader "LineWidths" + { + uniform token info:id = "ND_combine2_vector2" + float inputs:in1 = 0.1 + float inputs:in2 = 0.1 + float2 outputs:out + float2 ui:nodegraph:node:pos = (443.64453, 233.79297) + float2 ui:nodegraph:node:size = (151, 43) + int ui:nodegraph:node:stackingOrder = 1361 + } + + def Shader "LineCounts" + { + uniform token info:id = "ND_combine2_vector2" + float inputs:in1 = 24 + float inputs:in2 = 12 + float2 outputs:out + float2 ui:nodegraph:node:pos = (94.14453, 138.29297) + float2 ui:nodegraph:node:size = (153, 43) + int ui:nodegraph:node:stackingOrder = 1359 + } + + def Shader "Remap" + { + uniform token info:id = "ND_remap_color3" + color3f inputs:in.connect = + color3f inputs:inhigh.connect = None + color3f inputs:inlow.connect = None + color3f inputs:outhigh.connect = + color3f inputs:outlow.connect = + color3f outputs:out + float2 ui:nodegraph:node:pos = (1755.5, 300.5) + float2 ui:nodegraph:node:size = (95, 171) + int ui:nodegraph:node:stackingOrder = 1282 + string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["inputs:outlow"] + } + + def Shader "Separate2" + { + uniform token info:id = "ND_separate2_vector2" + float2 inputs:in.connect = + float outputs:outx + float outputs:outy + float2 ui:nodegraph:node:pos = (1212.6445, 128.91797) + float2 ui:nodegraph:node:size = (116, 117) + int ui:nodegraph:node:stackingOrder = 1363 + } + + def Shader "Combine3" + { + uniform token info:id = "ND_combine3_color3" + float inputs:in1.connect = + float inputs:in2.connect = + float inputs:in3.connect = + color3f outputs:out + float2 ui:nodegraph:node:pos = (1578.1445, 128.91797) + float2 ui:nodegraph:node:size = (146, 54) + int ui:nodegraph:node:stackingOrder = 1348 + } + + def Shader "Range" + { + uniform token info:id = "ND_range_vector2" + bool inputs:doclamp = 1 + float2 inputs:gamma = (2, 2) + float2 inputs:in.connect = + float2 inputs:inhigh.connect = + float2 inputs:inlow = (0.02, 0.02) + float2 inputs:outhigh + float2 inputs:outlow + float2 outputs:out + float2 ui:nodegraph:node:pos = (990.64453, 128.91797) + float2 ui:nodegraph:node:size = (98, 207) + int ui:nodegraph:node:stackingOrder = 1364 + } + + def Shader "Subtract" + { + uniform token info:id = "ND_subtract_vector2" + float2 inputs:in1.connect = + float2 inputs:in2.connect = + float2 outputs:out + float2 ui:nodegraph:node:pos = (612.64453, 87.04297) + float2 ui:nodegraph:node:size = (63, 36) + int ui:nodegraph:node:stackingOrder = 1348 + } + + def Shader "Absval" + { + uniform token info:id = "ND_absval_vector2" + float2 inputs:in.connect = + float2 outputs:out + float2 ui:nodegraph:node:pos = (765.64453, 87.04297) + float2 ui:nodegraph:node:size = (123, 43) + int ui:nodegraph:node:stackingOrder = 1348 + } + + def Shader "Min" + { + uniform token info:id = "ND_min_float" + float inputs:in1.connect = + float inputs:in2.connect = + float outputs:out + float2 ui:nodegraph:node:pos = (1388.1445, 128.91797) + float2 ui:nodegraph:node:size = (114, 36) + int ui:nodegraph:node:stackingOrder = 1363 + } + } +} + diff --git a/gym/examples/visionos/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Scene.usda b/gym/examples/visionos/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Scene.usda new file mode 100644 index 00000000000..4cb070bf444 --- /dev/null +++ b/gym/examples/visionos/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Scene.usda @@ -0,0 +1,59 @@ +#usda 1.0 +( + defaultPrim = "Root" + metersPerUnit = 1 + upAxis = "Y" +) + +def Xform "Root" +{ + reorder nameChildren = ["GridMaterial", "Sphere"] + rel material:binding = None ( + bindMaterialAs = "weakerThanDescendants" + ) + + def Sphere "Sphere" ( + active = true + prepend apiSchemas = ["MaterialBindingAPI"] + ) + { + rel material:binding = ( + bindMaterialAs = "weakerThanDescendants" + ) + double radius = 0.05 + quatf xformOp:orient = (1, 0, 0, 0) + float3 xformOp:scale = (1, 1, 1) + float3 xformOp:translate = (0, 0, 0.0004) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + + def RealityKitComponent "Collider" + { + uint group = 1 + uniform token info:id = "RealityKit.Collider" + uint mask = 4294967295 + token type = "Default" + + def RealityKitStruct "Shape" + { + float3 extent = (0.2, 0.2, 0.2) + float radius = 0.05 + token shapeType = "Sphere" + } + } + + def RealityKitComponent "InputTarget" + { + uniform token info:id = "RealityKit.InputTarget" + } + } + + def "GridMaterial" ( + active = true + prepend references = @Materials/GridMaterial.usda@ + ) + { + float3 xformOp:scale = (1, 1, 1) + uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] + } +} + diff --git a/gym/examples/visionos/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift b/gym/examples/visionos/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift new file mode 100644 index 00000000000..5caba4e3d2b --- /dev/null +++ b/gym/examples/visionos/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift @@ -0,0 +1,4 @@ +import Foundation + +/// Bundle for the RealityKitContent project +public let realityKitContentBundle = Bundle.module diff --git a/gym/examples/visionos/VisionExample.xcodeproj/project.pbxproj b/gym/examples/visionos/VisionExample.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..f1ab510e8d0 --- /dev/null +++ b/gym/examples/visionos/VisionExample.xcodeproj/project.pbxproj @@ -0,0 +1,367 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + E12346402AD8389B0010D449 /* RealityKitContent in Frameworks */ = {isa = PBXBuildFile; productRef = E123463F2AD8389B0010D449 /* RealityKitContent */; }; + E12346422AD8389B0010D449 /* Vision_ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12346412AD8389B0010D449 /* Vision_ExampleApp.swift */; }; + E12346442AD8389B0010D449 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12346432AD8389B0010D449 /* ContentView.swift */; }; + E12346462AD838A00010D449 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E12346452AD838A00010D449 /* Assets.xcassets */; }; + E12346492AD838A00010D449 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E12346482AD838A00010D449 /* Preview Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + E123463A2AD8389A0010D449 /* VisionExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VisionExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E123463E2AD8389B0010D449 /* RealityKitContent */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = RealityKitContent; sourceTree = ""; }; + E12346412AD8389B0010D449 /* Vision_ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vision_ExampleApp.swift; sourceTree = ""; }; + E12346432AD8389B0010D449 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + E12346452AD838A00010D449 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E12346482AD838A00010D449 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + E123464A2AD838A00010D449 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E12346372AD8389A0010D449 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E12346402AD8389B0010D449 /* RealityKitContent in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E12346312AD8389A0010D449 = { + isa = PBXGroup; + children = ( + E123463C2AD8389A0010D449 /* VisionExample */, + E123463D2AD8389B0010D449 /* Packages */, + E123463B2AD8389A0010D449 /* Products */, + ); + sourceTree = ""; + }; + E123463B2AD8389A0010D449 /* Products */ = { + isa = PBXGroup; + children = ( + E123463A2AD8389A0010D449 /* VisionExample.app */, + ); + name = Products; + sourceTree = ""; + }; + E123463C2AD8389A0010D449 /* VisionExample */ = { + isa = PBXGroup; + children = ( + E12346412AD8389B0010D449 /* Vision_ExampleApp.swift */, + E12346432AD8389B0010D449 /* ContentView.swift */, + E12346452AD838A00010D449 /* Assets.xcassets */, + E123464A2AD838A00010D449 /* Info.plist */, + E12346472AD838A00010D449 /* Preview Content */, + ); + path = VisionExample; + sourceTree = ""; + }; + E123463D2AD8389B0010D449 /* Packages */ = { + isa = PBXGroup; + children = ( + E123463E2AD8389B0010D449 /* RealityKitContent */, + ); + path = Packages; + sourceTree = ""; + }; + E12346472AD838A00010D449 /* Preview Content */ = { + isa = PBXGroup; + children = ( + E12346482AD838A00010D449 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E12346392AD8389A0010D449 /* VisionExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = E123464D2AD838A00010D449 /* Build configuration list for PBXNativeTarget "VisionExample" */; + buildPhases = ( + E12346362AD8389A0010D449 /* Sources */, + E12346372AD8389A0010D449 /* Frameworks */, + E12346382AD8389A0010D449 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = VisionExample; + packageProductDependencies = ( + E123463F2AD8389B0010D449 /* RealityKitContent */, + ); + productName = "Vision Example"; + productReference = E123463A2AD8389A0010D449 /* VisionExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E12346322AD8389A0010D449 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1510; + LastUpgradeCheck = 1510; + TargetAttributes = { + E12346392AD8389A0010D449 = { + CreatedOnToolsVersion = 15.1; + }; + }; + }; + buildConfigurationList = E12346352AD8389A0010D449 /* Build configuration list for PBXProject "VisionExample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E12346312AD8389A0010D449; + productRefGroup = E123463B2AD8389A0010D449 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E12346392AD8389A0010D449 /* VisionExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E12346382AD8389A0010D449 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E12346492AD838A00010D449 /* Preview Assets.xcassets in Resources */, + E12346462AD838A00010D449 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E12346362AD8389A0010D449 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E12346442AD8389B0010D449 /* ContentView.swift in Sources */, + E12346422AD8389B0010D449 /* Vision_ExampleApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + E123464B2AD838A00010D449 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = xros; + SUPPORTED_PLATFORMS = "xrsimulator xros"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + XROS_DEPLOYMENT_TARGET = 1.0; + }; + name = Debug; + }; + E123464C2AD838A00010D449 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = xros; + SUPPORTED_PLATFORMS = "xrsimulator xros"; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + XROS_DEPLOYMENT_TARGET = 1.0; + }; + name = Release; + }; + E123464E2AD838A00010D449 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Vision Example/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "$(TARGET_NAME)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.pppinki.Vision-Example"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "xrsimulator xros"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Debug; + }; + E123464F2AD838A00010D449 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Vision Example/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "$(TARGET_NAME)/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.pppinki.Vision-Example"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "xrsimulator xros"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E12346352AD8389A0010D449 /* Build configuration list for PBXProject "VisionExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E123464B2AD838A00010D449 /* Debug */, + E123464C2AD838A00010D449 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E123464D2AD838A00010D449 /* Build configuration list for PBXNativeTarget "VisionExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E123464E2AD838A00010D449 /* Debug */, + E123464F2AD838A00010D449 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + E123463F2AD8389B0010D449 /* RealityKitContent */ = { + isa = XCSwiftPackageProductDependency; + productName = RealityKitContent; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = E12346322AD8389A0010D449 /* Project object */; +} diff --git a/gym/examples/visionos/VisionExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/gym/examples/visionos/VisionExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000000..919434a6254 --- /dev/null +++ b/gym/examples/visionos/VisionExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/gym/examples/visionos/VisionExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/gym/examples/visionos/VisionExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000000..18d981003d6 --- /dev/null +++ b/gym/examples/visionos/VisionExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/gym/examples/visionos/VisionExample.xcodeproj/xcshareddata/xcschemes/VisionExample.xcscheme b/gym/examples/visionos/VisionExample.xcodeproj/xcshareddata/xcschemes/VisionExample.xcscheme new file mode 100644 index 00000000000..29c4dcd00a1 --- /dev/null +++ b/gym/examples/visionos/VisionExample.xcodeproj/xcshareddata/xcschemes/VisionExample.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..04056a547f7 --- /dev/null +++ b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "vision", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Contents.json b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Contents.json new file mode 100644 index 00000000000..950af4d85a8 --- /dev/null +++ b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Contents.json @@ -0,0 +1,17 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "layers" : [ + { + "filename" : "Front.solidimagestacklayer" + }, + { + "filename" : "Middle.solidimagestacklayer" + }, + { + "filename" : "Back.solidimagestacklayer" + } + ] +} diff --git a/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..04056a547f7 --- /dev/null +++ b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "vision", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json new file mode 100644 index 00000000000..04056a547f7 --- /dev/null +++ b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "vision", + "scale" : "2x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/gym/examples/visionos/VisionExample/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/gym/examples/visionos/VisionExample/Assets.xcassets/Contents.json b/gym/examples/visionos/VisionExample/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/gym/examples/visionos/VisionExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/gym/examples/visionos/VisionExample/ContentView.swift b/gym/examples/visionos/VisionExample/ContentView.swift new file mode 100644 index 00000000000..93f83278ee6 --- /dev/null +++ b/gym/examples/visionos/VisionExample/ContentView.swift @@ -0,0 +1,26 @@ +// +// ContentView.swift +// Vision Example +// +// Created by Philipp Resch on 12.10.23. +// + +import SwiftUI +import RealityKit +import RealityKitContent + +struct ContentView: View { + var body: some View { + VStack { + Model3D(named: "Scene", bundle: realityKitContentBundle) + .padding(.bottom, 50) + + Text("Hello, world!") + } + .padding() + } +} + +#Preview(windowStyle: .automatic) { + ContentView() +} diff --git a/gym/examples/visionos/VisionExample/Info.plist b/gym/examples/visionos/VisionExample/Info.plist new file mode 100644 index 00000000000..20f75e2afa7 --- /dev/null +++ b/gym/examples/visionos/VisionExample/Info.plist @@ -0,0 +1,15 @@ + + + + + UIApplicationSceneManifest + + UIApplicationPreferredDefaultSceneSessionRole + UIWindowSceneSessionRoleApplication + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + + + diff --git a/gym/examples/visionos/VisionExample/Preview Content/Preview Assets.xcassets/Contents.json b/gym/examples/visionos/VisionExample/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000000..73c00596a7f --- /dev/null +++ b/gym/examples/visionos/VisionExample/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/gym/examples/visionos/VisionExample/Vision_ExampleApp.swift b/gym/examples/visionos/VisionExample/Vision_ExampleApp.swift new file mode 100644 index 00000000000..637cff8ce65 --- /dev/null +++ b/gym/examples/visionos/VisionExample/Vision_ExampleApp.swift @@ -0,0 +1,17 @@ +// +// Vision_ExampleApp.swift +// Vision Example +// +// Created by Philipp Resch on 12.10.23. +// + +import SwiftUI + +@main +struct Vision_ExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/gym/lib/gym/detect_values.rb b/gym/lib/gym/detect_values.rb index 04a12004b24..4b7e6116043 100644 --- a/gym/lib/gym/detect_values.rb +++ b/gym/lib/gym/detect_values.rb @@ -147,6 +147,8 @@ def self.detect_platform platform = if Gym.project.tvos? "tvOS" + elsif Gym.project.visionos? + "visionOS" elsif Gym.building_for_ios? "iOS" elsif Gym.building_for_mac? diff --git a/gym/lib/gym/generators/build_command_generator.rb b/gym/lib/gym/generators/build_command_generator.rb index 3e5190db310..d836e759b6e 100644 --- a/gym/lib/gym/generators/build_command_generator.rb +++ b/gym/lib/gym/generators/build_command_generator.rb @@ -184,7 +184,7 @@ def archive_path def result_bundle_path unless Gym.cache[:result_bundle_path] path = Gym.config[:result_bundle_path] - path ||= File.join(Gym.config[:output_directory], Gym.config[:output_name] + ".result") + path ||= File.join(Gym.config[:output_directory], Gym.config[:output_name] + ".xcresult") if File.directory?(path) FileUtils.remove_dir(path) end diff --git a/gym/lib/gym/module.rb b/gym/lib/gym/module.rb index c790efb8750..266a84a5f4c 100644 --- a/gym/lib/gym/module.rb +++ b/gym/lib/gym/module.rb @@ -33,8 +33,8 @@ def building_for_ios? # Can be iOS project and build for mac if catalyst return false if building_mac_catalyst_for_mac? - # Can be iOS project if iOS, tvOS, or watchOS - return Gym.project.ios? || Gym.project.tvos? || Gym.project.watchos? + # Can be iOS project if iOS, tvOS, watchOS, or visionOS + return Gym.project.ios? || Gym.project.tvos? || Gym.project.watchos? || Gym.project.visionos? end end diff --git a/gym/lib/gym/options.rb b/gym/lib/gym/options.rb index 98041128b98..e7cdcbad7d9 100644 --- a/gym/lib/gym/options.rb +++ b/gym/lib/gym/options.rb @@ -320,7 +320,16 @@ def self.plain_options description: "Lets xcodebuild use system's scm configuration", optional: true, type: Boolean, - default_value: false) + default_value: false), + FastlaneCore::ConfigItem.new(key: :package_authorization_provider, + env_name: "GYM_PACKAGE_AUTHORIZATION_PROVIDER", + description: "Lets xcodebuild use a specified package authorization provider (keychain|netrc)", + optional: true, + type: String, + verify_block: proc do |value| + av = %w(netrc keychain) + UI.user_error!("Unsupported authorization provider '#{value}', must be: #{av}") unless av.include?(value) + end) ] end end diff --git a/gym/spec/build_command_generator_spec.rb b/gym/spec/build_command_generator_spec.rb index 6fe5a6093ed..4a86c145b8d 100644 --- a/gym/spec/build_command_generator_spec.rb +++ b/gym/spec/build_command_generator_spec.rb @@ -279,7 +279,7 @@ "-project ./gym/examples/standard/Example.xcodeproj", "-destination 'generic/platform=iOS'", "-archivePath #{Gym::BuildCommandGenerator.archive_path.shellescape}", - "-resultBundlePath './ExampleProductName.result'", + "-resultBundlePath './ExampleProductName.xcresult'", :archive, "| tee #{log_path.shellescape}", "| xcpretty" diff --git a/gym/spec/platform_detection_spec.rb b/gym/spec/platform_detection_spec.rb index f49043b2541..f677d3302f3 100644 --- a/gym/spec/platform_detection_spec.rb +++ b/gym/spec/platform_detection_spec.rb @@ -24,4 +24,17 @@ expect(Gym.building_for_mac?).to eq(true) expect(Gym.building_multiplatform_for_mac?).to eq(true) end + + it "detects the correct platform for a visionOS project", requires_xcodebuild: true, if: FastlaneCore::Helper.mac? && FastlaneCore::Helper.xcode_at_least?('15.0') do + options = { project: "./gym/examples/visionos/VisionExample.xcodeproj", sdk: "xros", skip_package_dependencies_resolution: true } + Gym.config = FastlaneCore::Configuration.create(Gym::Options.available_options, options) + + expect(Gym.project.multiplatform?).to eq(false) + expect(Gym.project.visionos?).to eq(true) + expect(Gym.project.ios?).to eq(false) + + expect(Gym.building_for_ios?).to eq(true) + expect(Gym.building_for_mac?).to eq(false) + expect(Gym.building_multiplatform_for_mac?).to eq(false) + end end diff --git a/match/lib/assets/READMETemplate.md b/match/lib/assets/READMETemplate.md index 059da4b3fa3..a12fbe8855d 100644 --- a/match/lib/assets/READMETemplate.md +++ b/match/lib/assets/READMETemplate.md @@ -14,13 +14,11 @@ Make sure you have the latest version of the Xcode command line tools installed: xcode-select --install ``` -Install _fastlane_ using +Install _fastlane_ using bundler by following instructions here on [fastlane docs](https://docs.fastlane.tools). -``` -[sudo] gem install fastlane -NV -``` +or alternatively using -or alternatively using `brew install fastlane` +`brew install fastlane` ### Usage diff --git a/match/lib/match/change_password.rb b/match/lib/match/change_password.rb index 32d220dce8c..8e765eab4b1 100644 --- a/match/lib/match/change_password.rb +++ b/match/lib/match/change_password.rb @@ -26,7 +26,8 @@ def self.update(params: nil) git_url: params[:git_url], s3_bucket: params[:s3_bucket], s3_skip_encryption: params[:s3_skip_encryption], - working_directory: storage.working_directory + working_directory: storage.working_directory, + force_legacy_encryption: params[:force_legacy_encryption] }) encryption.decrypt_files diff --git a/match/lib/match/encryption.rb b/match/lib/match/encryption.rb index 62bee369dcb..47a5d28fbcd 100644 --- a/match/lib/match/encryption.rb +++ b/match/lib/match/encryption.rb @@ -1,5 +1,6 @@ require_relative 'encryption/interface' require_relative 'encryption/openssl' +require_relative 'encryption/encryption' module Match module Encryption diff --git a/match/lib/match/encryption/encryption.rb b/match/lib/match/encryption/encryption.rb new file mode 100644 index 00000000000..8e793c7316d --- /dev/null +++ b/match/lib/match/encryption/encryption.rb @@ -0,0 +1,154 @@ +require 'base64' +require 'openssl' +require 'securerandom' + +module Match + module Encryption + # This is to keep backwards compatibility with the old fastlane version which used the local openssl installation. + # The encryption parameters in this implementation reflect the old behavior which was the most common default value in those versions. + # As for decryption, 1.0.x OpenSSL and earlier versions use MD5, 1.1.0c and newer uses SHA256, we try both before giving an error + class EncryptionV1 + ALGORITHM = 'aes-256-cbc' + + def encrypt(data:, password:, salt:, hash_algorithm: "MD5") + cipher = ::OpenSSL::Cipher.new(ALGORITHM) + cipher.encrypt + + keyivgen(cipher, password, salt, hash_algorithm) + + encrypted_data = cipher.update(data) + encrypted_data << cipher.final + { encrypted_data: encrypted_data } + end + + def decrypt(encrypted_data:, password:, salt:, hash_algorithm: "MD5") + cipher = ::OpenSSL::Cipher.new(ALGORITHM) + cipher.decrypt + + keyivgen(cipher, password, salt, hash_algorithm) + + data = cipher.update(encrypted_data) + data << cipher.final + end + + private + + def keyivgen(cipher, password, salt, hash_algorithm) + cipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm) + end + end + + # The newer encryption mechanism, which features a more secure key and IV generation. + # + # The IV is randomly generated and provided unencrypted. + # The salt should be randomly generated and provided unencrypted (like in the current implementation). + # The key is generated with OpenSSL::KDF::pbkdf2_hmac with properly chosen parameters. + # + # Short explanation about salt and IV: https://stackoverflow.com/a/1950674/6324550 + class EncryptionV2 + ALGORITHM = 'aes-256-gcm' + + def encrypt(data:, password:, salt:) + cipher = ::OpenSSL::Cipher.new(ALGORITHM) + cipher.encrypt + + keyivgen(cipher, password, salt) + + encrypted_data = cipher.update(data) + encrypted_data << cipher.final + + auth_tag = cipher.auth_tag + + { encrypted_data: encrypted_data, auth_tag: auth_tag } + end + + def decrypt(encrypted_data:, password:, salt:, auth_tag:) + cipher = ::OpenSSL::Cipher.new(ALGORITHM) + cipher.decrypt + + keyivgen(cipher, password, salt) + + cipher.auth_tag = auth_tag + + data = cipher.update(encrypted_data) + data << cipher.final + end + + private + + def keyivgen(cipher, password, salt) + keyIv = ::OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 10_000, length: 32 + 12 + 24, hash: "sha256") + key = keyIv[0..31] + iv = keyIv[32..43] + auth_data = keyIv[44..-1] + cipher.key = key + cipher.iv = iv + cipher.auth_data = auth_data + end + end + + class MatchDataEncryption + V1_PREFIX = "Salted__" + V2_PREFIX = "match_encrypted_v2__" + + def encrypt(data:, password:, version: 2) + salt = SecureRandom.random_bytes(8) + if version == 2 + e = EncryptionV2.new + encryption = e.encrypt(data: data, password: password, salt: salt) + encrypted_data = V2_PREFIX + salt + encryption[:auth_tag] + encryption[:encrypted_data] + else + e = EncryptionV1.new + encryption = e.encrypt(data: data, password: password, salt: salt) + encrypted_data = V1_PREFIX + salt + encryption[:encrypted_data] + end + Base64.encode64(encrypted_data) + end + + def decrypt(base64encoded_encrypted:, password:) + stored_data = Base64.decode64(base64encoded_encrypted) + if stored_data.start_with?(V2_PREFIX) + salt = stored_data[20..27] + auth_tag = stored_data[28..43] + data_to_decrypt = stored_data[44..-1] + e = EncryptionV2.new + e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, auth_tag: auth_tag) + else + salt = stored_data[8..15] + data_to_decrypt = stored_data[16..-1] + e = EncryptionV1.new + begin + # Note that we are not guaranteed to catch the decryption errors here if the password or the hash is wrong + # as there's no integrity checks. + # see https://github.com/fastlane/fastlane/issues/21663 + e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt) + # With the wrong hash_algorithm, there's here 0.4% chance that the decryption failure will go undetected + rescue => _ex + # With a wrong password, there's a 0.4% chance it will decrypt garbage and not fail + fallback_hash_algorithm = "SHA256" + e.decrypt(encrypted_data: data_to_decrypt, password: password, salt: salt, hash_algorithm: fallback_hash_algorithm) + end + end + end + end + + # The methods of this class will encrypt or decrypt files in place, by default. + class MatchFileEncryption + def encrypt(file_path:, password:, output_path: nil, version: 2) + output_path = file_path unless output_path + data_to_encrypt = File.binread(file_path) + e = MatchDataEncryption.new + data = e.encrypt(data: data_to_encrypt, password: password, version: version) + File.write(output_path, data) + end + + def decrypt(file_path:, password:, output_path: nil) + output_path = file_path unless output_path + content = File.read(file_path) + e = MatchDataEncryption.new + decrypted_data = e.decrypt(base64encoded_encrypted: content, password: password) + File.binwrite(output_path, decrypted_data) + end + end + end +end diff --git a/match/lib/match/encryption/openssl.rb b/match/lib/match/encryption/openssl.rb index 2c31a94a93b..5e8ab70434d 100644 --- a/match/lib/match/encryption/openssl.rb +++ b/match/lib/match/encryption/openssl.rb @@ -14,18 +14,23 @@ class OpenSSL < Interface attr_accessor :working_directory + attr_accessor :force_legacy_encryption + def self.configure(params) return self.new( keychain_name: params[:keychain_name], - working_directory: params[:working_directory] + working_directory: params[:working_directory], + force_legacy_encryption: params[:force_legacy_encryption] ) end # @param keychain_name: The identifier used to store the passphrase in the Keychain # @param working_directory: The path to where the certificates are stored - def initialize(keychain_name: nil, working_directory: nil) + # @param force_legacy_encryption: Force use of legacy EncryptionV1 algorithm + def initialize(keychain_name: nil, working_directory: nil, force_legacy_encryption: false) self.keychain_name = keychain_name self.working_directory = working_directory + self.force_legacy_encryption = force_legacy_encryption end def encrypt_files(password: nil) @@ -33,7 +38,7 @@ def encrypt_files(password: nil) password ||= fetch_password! iterate(self.working_directory) do |current| files << current - encrypt_specific_file(path: current, password: password) + encrypt_specific_file(path: current, password: password, version: force_legacy_encryption ? 1 : 2) UI.success("🔒 Encrypted '#{File.basename(current)}'") if FastlaneCore::Globals.verbose? end UI.success("🔒 Successfully encrypted certificates repo") @@ -109,25 +114,10 @@ def fetch_password! return password end - # We encrypt with MD5 because that was the most common default value in older fastlane versions which used the local OpenSSL installation - # A more secure key and IV generation is needed in the future - # IV should be randomly generated and provided unencrypted - # salt should be randomly generated and provided unencrypted (like in the current implementation) - # key should be generated with OpenSSL::KDF::pbkdf2_hmac with properly chosen parameters - # Short explanation about salt and IV: https://stackoverflow.com/a/1950674/6324550 - def encrypt_specific_file(path: nil, password: nil) + def encrypt_specific_file(path: nil, password: nil, version: nil) UI.user_error!("No password supplied") if password.to_s.strip.length == 0 - - data_to_encrypt = File.binread(path) - salt = SecureRandom.random_bytes(8) - - # The :: is important, as there is a name clash - cipher = ::OpenSSL::Cipher.new('AES-256-CBC') - cipher.encrypt - cipher.pkcs5_keyivgen(password, salt, 1, "MD5") - encrypted_data = "Salted__" + salt + cipher.update(data_to_encrypt) + cipher.final - - File.write(path, Base64.encode64(encrypted_data)) + e = MatchFileEncryption.new + e.encrypt(file_path: path, password: password, version: version) rescue FastlaneCore::Interface::FastlaneError raise rescue => error @@ -135,28 +125,12 @@ def encrypt_specific_file(path: nil, password: nil) UI.crash!("Error encrypting '#{path}'") end - # The encryption parameters in this implementations reflect the old behavior which depended on the users' local OpenSSL version - # 1.0.x OpenSSL and earlier versions use MD5, 1.1.0c and newer uses SHA256, we try both before giving an error - def decrypt_specific_file(path: nil, password: nil, hash_algorithm: "MD5") - stored_data = Base64.decode64(File.read(path)) - salt = stored_data[8..15] - data_to_decrypt = stored_data[16..-1] - - decipher = ::OpenSSL::Cipher.new('AES-256-CBC') - decipher.decrypt - decipher.pkcs5_keyivgen(password, salt, 1, hash_algorithm) - - decrypted_data = decipher.update(data_to_decrypt) + decipher.final - - File.binwrite(path, decrypted_data) + def decrypt_specific_file(path: nil, password: nil) + e = MatchFileEncryption.new + e.decrypt(file_path: path, password: password) rescue => error - fallback_hash_algorithm = "SHA256" - if hash_algorithm != fallback_hash_algorithm - decrypt_specific_file(path: path, password: password, hash_algorithm: fallback_hash_algorithm) - else - UI.error(error.to_s) - UI.crash!("Error decrypting '#{path}'") - end + UI.error(error.to_s) + UI.crash!("Error decrypting '#{path}'") end end end diff --git a/match/lib/match/importer.rb b/match/lib/match/importer.rb index 7998918a73e..e3d78491b6c 100644 --- a/match/lib/match/importer.rb +++ b/match/lib/match/importer.rb @@ -23,7 +23,8 @@ def import_cert(params, cert_path: nil, p12_path: nil, profile_path: nil) git_url: params[:git_url], s3_bucket: params[:s3_bucket], s3_skip_encryption: params[:s3_skip_encryption], - working_directory: storage.working_directory + working_directory: storage.working_directory, + force_legacy_encryption: params[:force_legacy_encryption] }) encryption.decrypt_files if encryption UI.success("Repo is at: '#{storage.working_directory}'") diff --git a/match/lib/match/nuke.rb b/match/lib/match/nuke.rb index 96547492a3e..2825a63013e 100644 --- a/match/lib/match/nuke.rb +++ b/match/lib/match/nuke.rb @@ -42,7 +42,8 @@ def run(params, type: nil) git_url: params[:git_url], s3_bucket: params[:s3_bucket], s3_skip_encryption: params[:s3_skip_encryption], - working_directory: storage.working_directory + working_directory: storage.working_directory, + force_legacy_encryption: params[:force_legacy_encryption] }) self.encryption.decrypt_files if self.encryption diff --git a/match/lib/match/options.rb b/match/lib/match/options.rb index bb538eba388..4ffd1c161de 100644 --- a/match/lib/match/options.rb +++ b/match/lib/match/options.rb @@ -355,6 +355,11 @@ def self.available_options description: "Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing", type: Boolean, default_value: false), + FastlaneCore::ConfigItem.new(key: :force_legacy_encryption, + env_name: "MATCH_FORCE_LEGACY_ENCRYPTION", + description: "Force encryption to use legacy cbc algorithm for backwards compatibility with older match versions", + type: Boolean, + default_value: false), # other FastlaneCore::ConfigItem.new(key: :verbose, diff --git a/match/lib/match/runner.rb b/match/lib/match/runner.rb index 7151dd78ec1..905ef8dd4c7 100644 --- a/match/lib/match/runner.rb +++ b/match/lib/match/runner.rb @@ -19,6 +19,7 @@ module Match # rubocop:disable Metrics/ClassLength class Runner attr_accessor :files_to_commit + attr_accessor :files_to_delete attr_accessor :spaceship attr_accessor :storage @@ -28,6 +29,7 @@ class Runner # rubocop:disable Metrics/PerceivedComplexity def run(params) self.files_to_commit = [] + self.files_to_delete = [] FileUtils.mkdir_p(params[:output_path]) if params[:output_path] @@ -47,7 +49,8 @@ def run(params) git_url: params[:git_url], s3_bucket: params[:s3_bucket], s3_skip_encryption: params[:s3_skip_encryption], - working_directory: storage.working_directory + working_directory: storage.working_directory, + force_legacy_encryption: params[:force_legacy_encryption] }) encryption.decrypt_files if encryption @@ -82,12 +85,12 @@ def run(params) end # Certificate - cert_id = fetch_certificate(params: params, working_directory: storage.working_directory) + cert_id = fetch_certificate(params: params, renew_expired_certs: false) # Mac Installer Distribution Certificate additional_cert_types = params[:additional_cert_types] || [] cert_ids = additional_cert_types.map do |additional_cert_type| - fetch_certificate(params: params, working_directory: storage.working_directory, specific_cert_type: additional_cert_type) + fetch_certificate(params: params, renew_expired_certs: false, specific_cert_type: additional_cert_type) end profile_type = Sigh.profile_type_for_distribution_type( @@ -112,9 +115,10 @@ def run(params) end end - if self.files_to_commit.count > 0 && !params[:readonly] + has_file_changes = self.files_to_commit.count > 0 || self.files_to_delete.count > 0 + if has_file_changes && !params[:readonly] encryption.encrypt_files if encryption - storage.save_changes!(files_to_commit: self.files_to_commit) + storage.save_changes!(files_to_commit: self.files_to_commit, files_to_delete: self.files_to_delete) end # Print a summary table for each app_identifier @@ -152,13 +156,48 @@ def update_optional_values_depending_on_storage_type(params) end end - def fetch_certificate(params: nil, working_directory: nil, specific_cert_type: nil) + RENEWABLE_CERT_TYPES_VIA_API = [:mac_installer_distribution, :development, :distribution, :enterprise] + + def fetch_certificate(params: nil, renew_expired_certs: false, specific_cert_type: nil) cert_type = Match.cert_type_sym(specific_cert_type || params[:type]) certs = Dir[File.join(prefixed_working_directory, "certs", cert_type.to_s, "*.cer")] keys = Dir[File.join(prefixed_working_directory, "certs", cert_type.to_s, "*.p12")] - if certs.count == 0 || keys.count == 0 + storage_has_certs = certs.count != 0 && keys.count != 0 + + # Determine if cert is renewable. + # Can't renew developer_id certs with Connect API token. Account holder access is required. + is_authenticated_with_login = Spaceship::ConnectAPI.token.nil? + is_cert_renewable_via_api = RENEWABLE_CERT_TYPES_VIA_API.include?(cert_type) + is_cert_renewable = is_authenticated_with_login || is_cert_renewable_via_api + + # Validate existing certificate first. + if renew_expired_certs && is_cert_renewable && storage_has_certs && !params[:readonly] + cert_path = select_cert_or_key(paths: certs) + + unless Utils.is_cert_valid?(cert_path) + UI.important("Removing invalid certificate '#{File.basename(cert_path)}'") + + # Remove expired cert. + self.files_to_delete << cert_path + File.delete(cert_path) + + # Key filename is the same as cert but with .p12 extension. + key_path = cert_path.gsub(/\.cer$/, ".p12") + # Remove expired key .p12 file. + if File.exist?(key_path) + self.files_to_delete << key_path + File.delete(key_path) + end + + certs = [] + keys = [] + storage_has_certs = false + end + end + + if !storage_has_certs UI.important("Couldn't find a valid code signing identity for #{cert_type}... creating one for you now") UI.crash!("No code signing identity found and cannot create a new one because you enabled `readonly`") if params[:readonly] cert_path = Generator.generate_certificate(params, cert_type, prefixed_working_directory, specific_cert_type: specific_cert_type) diff --git a/match/lib/match/storage/git_storage.rb b/match/lib/match/storage/git_storage.rb index 24bd0703898..4d9f1223fd3 100644 --- a/match/lib/match/storage/git_storage.rb +++ b/match/lib/match/storage/git_storage.rb @@ -140,9 +140,10 @@ def human_readable_description end def delete_files(files_to_delete: [], custom_message: nil) - # No specific list given, e.g. this happens on `fastlane match nuke` - # We just want to run `git add -A` to commit everything - git_push(commands: ["git add -A"], commit_message: custom_message) + if files_to_delete.count > 0 + commands = files_to_delete.map { |filename| "git rm #{filename.shellescape}" } + git_push(commands: commands, commit_message: custom_message) + end end def upload_files(files_to_upload: [], custom_message: nil) diff --git a/match/lib/match/storage/interface.rb b/match/lib/match/storage/interface.rb index 2c6e7b0f0da..67d763cc916 100644 --- a/match/lib/match/storage/interface.rb +++ b/match/lib/match/storage/interface.rb @@ -55,11 +55,14 @@ def save_changes!(files_to_commit: nil, files_to_delete: nil, custom_message: ni # Custom init to `[]` in case `nil` is passed files_to_commit ||= [] files_to_delete ||= [] + files_to_delete -= files_to_commit # Make sure we are not removing added files. + + if files_to_commit.count == 0 && files_to_delete.count == 0 + UI.user_error!("Neither `files_to_commit` nor `files_to_delete` were provided to the `save_changes!` method call") + end Dir.chdir(File.expand_path(self.working_directory)) do if files_to_commit.count > 0 # everything that isn't `match nuke` - UI.user_error!("You can't provide both `files_to_delete` and `files_to_commit` right now") if files_to_delete.count > 0 - if !File.exist?(MATCH_VERSION_FILE_NAME) || File.read(MATCH_VERSION_FILE_NAME) != Fastlane::VERSION.to_s files_to_commit << MATCH_VERSION_FILE_NAME File.write(MATCH_VERSION_FILE_NAME, Fastlane::VERSION) # stored unencrypted @@ -74,13 +77,14 @@ def save_changes!(files_to_commit: nil, files_to_delete: nil, custom_message: ni self.upload_files(files_to_upload: files_to_commit, custom_message: custom_message) UI.message("Finished uploading files to #{self.human_readable_description}") - elsif files_to_delete.count > 0 + end + + if files_to_delete.count > 0 self.delete_files(files_to_delete: files_to_delete, custom_message: custom_message) UI.message("Finished deleting files from #{self.human_readable_description}") - else - UI.user_error!("Neither `files_to_commit` nor `files_to_delete` were provided to the `save_changes!` method call") end end + ensure # Always clear working_directory after save self.clear_changes end diff --git a/match/lib/match/storage/s3_storage.rb b/match/lib/match/storage/s3_storage.rb index 44fb4dc88b6..c9b96a20235 100644 --- a/match/lib/match/storage/s3_storage.rb +++ b/match/lib/match/storage/s3_storage.rb @@ -101,8 +101,11 @@ def download # No existing working directory, creating a new one now self.working_directory = Dir.mktmpdir - s3_client.find_bucket!(s3_bucket).objects(prefix: s3_object_prefix).each do |object| + # If team_id is defined, use `:team/` as a prefix (appending it at the end of the `s3_object_prefix` if one provided by the user), + # so that we limit the download to only files that are specific to this team, and avoid downloads + decryption of unnecessary files. + key_prefix = team_id.nil? ? s3_object_prefix : File.join(s3_object_prefix, team_id, '').delete_prefix('/') + s3_client.find_bucket!(s3_bucket).objects(prefix: key_prefix).each do |object| # Prevent download if the file path is a directory. # We need to check if string ends with "/" instead of using `File.directory?` because # the string represent a remote location, not a local file in disk. @@ -181,7 +184,7 @@ def s3_object_path(file_name) end def strip_s3_object_prefix(object_path) - object_path.gsub(/^#{s3_object_prefix}/, "") + object_path.delete_prefix(s3_object_prefix.to_s).delete_prefix('/') end def sanitize_file_name(file_name) diff --git a/match/spec/encryption/encryption_spec.rb b/match/spec/encryption/encryption_spec.rb new file mode 100644 index 00000000000..08f5deb8157 --- /dev/null +++ b/match/spec/encryption/encryption_spec.rb @@ -0,0 +1,47 @@ +describe Match do + describe Match::Encryption::MatchDataEncryption do + let(:v1) { Match::Encryption::EncryptionV1.new } + let(:v2) { Match::Encryption::EncryptionV2.new } + let(:e) { Match::Encryption::MatchDataEncryption.new } + let(:salt) { salt = SecureRandom.random_bytes(8) } + + let(:data) { "Hello World" } + let(:password) { '2"QAHg@v(Qp{=*n^' } + + it "decrypts V1 Encryption with default hash" do + encryption = v1.encrypt(data: data, password: password, salt: salt) + encrypted_data = Match::Encryption::MatchDataEncryption::V1_PREFIX + salt + encryption[:encrypted_data] + encoded_encrypted_data = Base64.encode64(encrypted_data) + + expect(e.decrypt(base64encoded_encrypted: encoded_encrypted_data, password: password)).to eq(data) + end + + it "decrypts V1 Encryption with SHA256 hash when the decryption fails when given the default hash" do + encryption = v1.encrypt(data: data, password: password, salt: salt, hash_algorithm: "SHA256") + encrypted_data = Match::Encryption::MatchDataEncryption::V1_PREFIX + salt + encryption[:encrypted_data] + encoded_encrypted_data = Base64.encode64(encrypted_data) + + eMock = double("EncryptionV1") + # make it fail with the default hash + allow(eMock).to receive(:decrypt).with(encrypted_data: encryption[:encrypted_data], password: password, salt: salt).and_raise(OpenSSL::Cipher::CipherError) + allow(eMock).to receive(:decrypt).with(encrypted_data: encryption[:encrypted_data], password: password, salt: salt, hash_algorithm: "SHA256").and_return(data) + + allow(Match::Encryption::EncryptionV1).to receive(:new).and_return(eMock) + expect(e.decrypt(base64encoded_encrypted: encoded_encrypted_data, password: password)).to eq(data) + end + + it "fails to decrypt V1 Encryption with SHA256 hash when the decryption doesn't fail when given the default hash" do + encryption = v1.encrypt(data: data, password: password, salt: salt, hash_algorithm: "SHA256") + encrypted_data = Match::Encryption::MatchDataEncryption::V1_PREFIX + salt + encryption[:encrypted_data] + encoded_encrypted_data = Base64.encode64(encrypted_data) + + eMock = double("EncryptionV1") + # make it return garbage + garbage = "garbage" + allow(eMock).to receive(:decrypt).with(encrypted_data: encryption[:encrypted_data], password: password, salt: salt).and_return(garbage) + + allow(Match::Encryption::EncryptionV1).to receive(:new).and_return(eMock) + expect(e.decrypt(base64encoded_encrypted: encoded_encrypted_data, password: password)).to eq(garbage) + end + end +end diff --git a/match/spec/encryption/openssl_spec.rb b/match/spec/encryption/openssl_spec.rb index 44d15ad329a..bec43efeef9 100644 --- a/match/spec/encryption/openssl_spec.rb +++ b/match/spec/encryption/openssl_spec.rb @@ -59,5 +59,52 @@ @e.decrypt_files expect(File.binread(@full_path)).to eq(@content) end + + describe "behavior of force_legacy_encryption parameter" do + + before do + @match_encryption_double = instance_double(Match::Encryption::MatchFileEncryption) + + expect(Match::Encryption::MatchFileEncryption) + .to(receive(:new)) + .and_return(@match_encryption_double) + end + + it "defaults to false and uses v2 encryption" do + expect(@match_encryption_double) + .to(receive(:encrypt)) + .with(file_path: anything, password: anything, version: 2) + + @e.encrypt_files + end + + it "uses v1 when force_legacy_encryption is true" do + enc = Match::Encryption::OpenSSL.new( + keychain_name: @git_url, + working_directory: @directory, + force_legacy_encryption: true + ) + + expect(@match_encryption_double) + .to(receive(:encrypt)) + .with(file_path: anything, password: anything, version: 1) + + enc.encrypt_files + end + + it "uses v2 when force_legacy_encryption is false" do + enc = Match::Encryption::OpenSSL.new( + keychain_name: @git_url, + working_directory: @directory, + force_legacy_encryption: false + ) + + expect(@match_encryption_double) + .to(receive(:encrypt)) + .with(file_path: anything, password: anything, version: 2) + + enc.encrypt_files + end + end end end diff --git a/match/spec/fixtures/invalid/certs/distribution/F7P4EE896K.cer b/match/spec/fixtures/invalid/certs/distribution/F7P4EE896K.cer new file mode 100644 index 00000000000..fb48b2040d6 Binary files /dev/null and b/match/spec/fixtures/invalid/certs/distribution/F7P4EE896K.cer differ diff --git a/match/spec/fixtures/invalid/certs/distribution/F7P4EE896K.p12 b/match/spec/fixtures/invalid/certs/distribution/F7P4EE896K.p12 new file mode 100644 index 00000000000..0b305631e09 --- /dev/null +++ b/match/spec/fixtures/invalid/certs/distribution/F7P4EE896K.p12 @@ -0,0 +1,24 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAyTm/v40AZb6I1eXRPVaUF+q683gm+XTRaRC9fjqK3seL117n +gLte6+YOihzt88v7uJvEP0NN5pFLU4x8v/s+S/VC9Rp2Qd7CZIU1P+LJVWbIjJ31 +HPW9vVPLILbFERgTE8IblCkUa52KLcegTkvpqE/uS+ERXCsQM8FpK2urMHvIisCa +c2f7O+B/7my+DOaAQaAEqvQtaIxMIIEogIBAAKCAQEAyTm/v40AZb6I1eXRPVaUF+q683gm+XTRaRC9fjqK3seL117n +gLte6+YOihzt88v7uJvEP0NN5pFLU4x8v/s+S/VC9Rp2Qd7CZIU1P+LJVWbIjJ31 +HPW9vVPLILbFERgTE8IblCkUa52KLcegTkvpqE/uS+ERXCsQM8FpK2urMHvIisCa +c2f7O+B/7my+DOaAQaAEqvQtaIxMIIEogIBAAKCAQEAyTm/v40AZb6I1eXRPVaUF+q683gm+XTRaRC9fjqK3seL117n +gLte6+YOihzt88v7uJvEP0NN5pFLU4x8v/s+S/VC9Rp2Qd7CZIU1P+LJVWbIjJ31 +HPW9vVPLILbFERgTE8IblCkUa52KLcegTkvpqE/uS+ERXCsQM8FpK2urMHvIisCa +c2f7O+B/7my+DOaAQaAEqvQtaIxMIIEogIBAAKCAQEAyTm/v40AZb6I1eXRPVaUF+q683gm+XTRaRC9fjqK3seL117n +gLte6+YOihzt88v7uJvEP0NN5pFLU4x8v/s+S/VC9Rp2Qd7CZIU1P+LJVWbIjJ31 +HPW9vVPLILbFERgTE8IblCkUa52KLcegTkvpqE/uS+ERXCsQM8FpK2urMHvIisCa +c2f7O+B/7my+DOaAQaAEqvQtaIxMIIEogIBAAKCAQEAyTm/v40AZb6I1eXRPVaUF+q683gm+XTRaRC9fjqK3seL117n +gLte6+YOihzt88v7uJvEP0NN5pFLU4x8v/s+S/VC9Rp2Qd7CZIU1P+LJVWbIjJ31 +HPW9vVPLILbFERgTE8IblCkUa52KLcegTkvpqE/uS+ERXCsQM8FpK2urMHvIisCa +c2f7O+B/7my+DOaAQaAEqvQtaIxMIIEogIBAAKCAQEAyTm/v40AZb6I1eXRPVaUF+q683gm+XTRaRC9fjqK3seL117n +gLte6+YOihzt88v7uJvEP0NN5pFLU4x8v/s+S/VC9Rp2Qd7CZIU1P+LJVWbIjJ31 +HPW9vVPLILbFERgTE8IblCkUa52KLcegTkvpqE/uS+ERXCsQM8FpK2urMHvIisCa +c2f7O+B/7my+DOaAQaAEqvQtaIxMIIEogIBAAKCAQEAyTm/v40AZb6I1eXRPVaUF+q683gm+XTRaRC9fjqK3seL117n +gLte6+YOihzt88v7uJvEP0NN5pFLU4x8v/s+S/VC9Rp2Qd7CZIU1P+LJVWbIjJ31 +HPW9vVPLILbFERgTE8IblCkUa52KLcegTkvpqE/uS+ERXCsQM8FpK2urMHvIisCa +c2f7O+B/7my+DOaAQaAEqvQtaIx +-----END RSA PRIVATE KEY----- diff --git a/match/spec/runner_spec.rb b/match/spec/runner_spec.rb index c23c297e8ed..17393ae4e3a 100644 --- a/match/spec/runner_spec.rb +++ b/match/spec/runner_spec.rb @@ -77,7 +77,8 @@ File.join(repo_dir, "something.cer"), File.join(repo_dir, "something.p12"), # this is important, as a cert consists out of 2 files "./match/spec/fixtures/test.mobileprovision" - ] + ], + files_to_delete: [] ) spaceship = "spaceship" @@ -142,7 +143,7 @@ allow(fake_storage).to receive(:prefixed_working_directory).and_return(repo_dir) fake_encryption = "fake_encryption" - expect(Match::Encryption::OpenSSL).to receive(:new).with(keychain_name: fake_storage.git_url, working_directory: fake_storage.working_directory).and_return(fake_encryption) + expect(Match::Encryption::OpenSSL).to receive(:new).with(keychain_name: fake_storage.git_url, working_directory: fake_storage.working_directory, force_legacy_encryption: false).and_return(fake_encryption) expect(fake_encryption).to receive(:decrypt_files).and_return(nil) expect(Match::Utils).to receive(:import).with(key_path, keychain, password: nil).and_return(nil) @@ -178,13 +179,14 @@ type: "appstore")]).to eql("fastlane certificate name") end - it "fails because of an outdated certificate", requires_security: true do + it "fails because of an outdated certificate in readonly mode", requires_security: true do git_url = "https://github.com/fastlane/fastlane/tree/master/certificates" values = { app_identifier: "tools.fastlane.app", type: "appstore", git_url: git_url, - username: "flapple@something.com" + username: "flapple@something.com", + readonly: true } config = FastlaneCore::Configuration.create(Match::Options.available_options, values) @@ -194,36 +196,15 @@ create_fake_cache - fake_storage = "fake_storage" - expect(Match::Storage::GitStorage).to receive(:configure).with({ - git_url: git_url, - shallow_clone: false, - skip_docs: false, - git_branch: "master", - git_full_name: nil, - git_user_email: nil, - clone_branch_directly: false, - git_basic_authorization: nil, - git_bearer_authorization: nil, - git_private_key: nil, - type: config[:type], - platform: config[:platform] - }).and_return(fake_storage) - - expect(fake_storage).to receive(:download).and_return(nil) - expect(fake_storage).to receive(:clear_changes).and_return(nil) - allow(fake_storage).to receive(:git_url).and_return(git_url) - allow(fake_storage).to receive(:working_directory).and_return(repo_dir) - allow(fake_storage).to receive(:prefixed_working_directory).and_return(repo_dir) + fake_storage = create_fake_storage(match_config: config, repo_dir: repo_dir) fake_encryption = "fake_encryption" - expect(Match::Encryption::OpenSSL).to receive(:new).with(keychain_name: fake_storage.git_url, working_directory: fake_storage.working_directory).and_return(fake_encryption) + expect(Match::Encryption::OpenSSL).to receive(:new).with(keychain_name: fake_storage.git_url, working_directory: fake_storage.working_directory, force_legacy_encryption: false).and_return(fake_encryption) expect(fake_encryption).to receive(:decrypt_files).and_return(nil) spaceship = "spaceship" allow(spaceship).to receive(:team_id).and_return("") - expect(Match::SpaceshipEnsure).to receive(:new).and_return(spaceship) - expect(spaceship).to receive(:bundle_identifier_exists).and_return(true) + expect(Match::SpaceshipEnsure).not_to receive(:new) expect(Match::Utils).to receive(:is_cert_valid?).and_return(false) @@ -243,6 +224,7 @@ # match options match_test_options = { + output_path: "tmp/match_certs", # to test the expectation that we do a file copy when this option is provided readonly: true # Current test suite. } match_config = create_match_config_with_git_storage(extra_values: match_test_options) @@ -277,6 +259,12 @@ expect(Match::Generator).not_to receive(:generate_provisioning_profile) end + # output_path + expect(FileUtils).to receive(:mkdir_p).with(match_test_options[:output_path]) + expect(FileUtils).to receive(:cp).with(stored_valid_cert_path, match_test_options[:output_path]) + expect(FileUtils).to receive(:cp).with("#{repo_dir}/certs/distribution/E7P4EE896K.p12", match_test_options[:output_path]) + expect(FileUtils).to receive(:cp).with(stored_valid_profile_path, match_test_options[:output_path]) + # WHEN Match::Runner.new.run(match_config) @@ -330,7 +318,8 @@ files_to_commit: [ File.join(repo_dir, "something.cer"), File.join(repo_dir, "something.p12") # this is important, as a cert consists out of 2 files - ] + ], + files_to_delete: [] ) spaceship = "spaceship" diff --git a/match/spec/spec_helper.rb b/match/spec/spec_helper.rb index 616094a53e3..80cc23f9c19 100644 --- a/match/spec/spec_helper.rb +++ b/match/spec/spec_helper.rb @@ -12,13 +12,13 @@ def before_each_match def create_fake_storage(match_config:, repo_dir:) fake_storage = "fake_storage" expect(Match::Storage::GitStorage).to receive(:configure).with({ - git_url: match_config[:git_url], - shallow_clone: true, - skip_docs: false, - git_branch: "master", + git_url: match_config[:git_url] || default_git_url, + shallow_clone: match_config[:shallow_clone] || false, + skip_docs: match_config[:skip_docs] || false, + git_branch: match_config[:git_branch] || "master", git_full_name: nil, git_user_email: nil, - clone_branch_directly: false, + clone_branch_directly: match_config[:clone_branch_directly] || false, git_basic_authorization: nil, git_bearer_authorization: nil, git_private_key: nil, @@ -74,7 +74,7 @@ def create_match_config_with_git_storage(extra_values: {}, git_url: nil, app_ide def create_fake_encryption(storage:) fake_encryption = "fake_encryption" - expect(Match::Encryption::OpenSSL).to receive(:new).with(keychain_name: storage.git_url, working_directory: storage.working_directory).and_return(fake_encryption) + expect(Match::Encryption::OpenSSL).to receive(:new).with(keychain_name: storage.git_url, working_directory: storage.working_directory, force_legacy_encryption: false).and_return(fake_encryption) # Ensure files from storage are decrypted. expect(fake_encryption).to receive(:decrypt_files).and_return(nil) diff --git a/match/spec/storage/gitlab/client_spec.rb b/match/spec/storage/gitlab/client_spec.rb index e55155ffb42..1c06b672a0f 100644 --- a/match/spec/storage/gitlab/client_spec.rb +++ b/match/spec/storage/gitlab/client_spec.rb @@ -101,7 +101,7 @@ { id: 2, name: 'file2' } ].to_json - stub_request(:get, /gitlab.example.com/). + stub_request(:get, /gitlab\.example\.com/). with(headers: { 'PRIVATE-TOKEN' => 'abc123' }). to_return(status: 200, body: response) @@ -111,7 +111,7 @@ end it 'returns an empty array if there are results' do - stub_request(:get, /gitlab.example.com/). + stub_request(:get, /gitlab\.example\.com/). with(headers: { 'PRIVATE-TOKEN' => 'abc123' }). to_return(status: 200, body: [].to_json) @@ -119,7 +119,7 @@ end it 'requests 100 files from the API' do - stub_request(:get, /gitlab.example.com/). + stub_request(:get, /gitlab\.example\.com/). to_return(status: 200, body: [].to_json) files = subject.files @@ -128,7 +128,7 @@ end it 'raises an exception for a non-json response' do - stub_request(:get, /gitlab.example.com/). + stub_request(:get, /gitlab\.example\.com/). with(headers: { 'PRIVATE-TOKEN' => 'abc123' }). to_return(status: 200, body: 'foo') diff --git a/match/spec/storage/s3_storage_spec.rb b/match/spec/storage/s3_storage_spec.rb index 88c23520468..c208077c9fa 100644 --- a/match/spec/storage/s3_storage_spec.rb +++ b/match/spec/storage/s3_storage_spec.rb @@ -70,15 +70,25 @@ describe '#download' do let(:files_to_download) do [ - instance_double('Aws::S3::Object', key: 'ABCDEFG/certs/development/ABCDEFG.cer', download_file: true), - instance_double('Aws::S3::Object', key: 'ABCDEFG/certs/development/ABCDEFG.p12', download_file: true) + instance_double('Aws::S3::Object', key: 'TEAMID1/certs/development/CERTID1.cer', download_file: true), + instance_double('Aws::S3::Object', key: 'TEAMID1/certs/development/CERTID1.p12', download_file: true), + instance_double('Aws::S3::Object', key: 'TEAMID2/certs/development/CERTID2.cer', download_file: true), + instance_double('Aws::S3::Object', key: 'TEAMID2/certs/development/CERTID2.p12', download_file: true) ] end - let(:s3_client) { instance_double('Fastlane::Helper::S3ClientHelper', find_bucket!: double(objects: files_to_download)) } + let(:bucket) { instance_double('Aws::S3::Bucket') } + let(:s3_client) { instance_double('Fastlane::Helper::S3ClientHelper', find_bucket!: bucket) } + + def stub_bucket_content(objects: files_to_download) + allow(bucket).to receive(:objects) do |options| + objects.select { |file_object| file_object.key.start_with?(options[:prefix] || '') } + end + end before { class_double('FileUtils', mkdir_p: true).as_stubbed_const } it 'downloads to correct working directory' do + stub_bucket_content files_to_download.each do |file_object| expect(file_object).to receive(:download_file).with("#{working_directory}/#{file_object.key}") end @@ -86,11 +96,30 @@ subject.download end + it 'only downloads files specific to the provided team' do + stub_bucket_content + allow(subject).to receive(:team_id).and_return('TEAMID2') + files_to_download.each do |file_object| + if file_object.key.start_with?('TEAMID2') + expect(file_object).to receive(:download_file).with("#{working_directory}/#{file_object.key}") + else + expect(file_object).not_to receive(:download_file) + end + end + + subject.download + end + it 'downloads files and strips the s3_object_prefix for working_directory path' do allow(subject).to receive(:s3_object_prefix).and_return('123456/') - files_to_download.each do |file_object| - expect(file_object).to receive(:download_file).with("#{working_directory}/#{file_object.key}") + prefixed_objects = files_to_download.map do |obj| + instance_double('Aws::S3::Object', key: "123456/#{obj.key}", download_file: true) + end + stub_bucket_content(objects: prefixed_objects) + + prefixed_objects.each do |file_object| + expect(file_object).to receive(:download_file).with("#{working_directory}/#{file_object.key.delete_prefix('123456/')}") end subject.download diff --git a/pilot/lib/pilot/build_manager.rb b/pilot/lib/pilot/build_manager.rb index 61cc3b2b08a..9d51511e1bf 100644 --- a/pilot/lib/pilot/build_manager.rb +++ b/pilot/lib/pilot/build_manager.rb @@ -65,6 +65,7 @@ def upload(options) UI.important("`skip_waiting_for_build_processing` used and no `changelog` supplied - skipping waiting for build processing") return else + UI.important("`skip_waiting_for_build_processing` used and `changelog` supplied - will wait until build appears on App Store Connect, update the changelog and then skip the rest of the remaining of the processing steps.") return_when_build_appears = true end end @@ -72,8 +73,10 @@ def upload(options) # Calling login again here is needed if login was not called during 'start' login unless should_login_in_start - UI.message("If you want to skip waiting for the processing to be finished, use the `skip_waiting_for_build_processing` option") - UI.message("Note that if `skip_waiting_for_build_processing` is used but a `changelog` is supplied, this process will wait for the build to appear on AppStoreConnect, update the changelog and then skip the remaining of the processing steps.") + if config[:skip_waiting_for_build_processing].nil? + UI.message("If you want to skip waiting for the processing to be finished, use the `skip_waiting_for_build_processing` option") + UI.message("Note that if `skip_waiting_for_build_processing` is used but a `changelog` is supplied, this process will wait for the build to appear on App Store Connect, update the changelog and then skip the remaining of the processing steps.") + end latest_build = wait_for_build_processing_to_be_complete(return_when_build_appears) distribute(options, build: latest_build) @@ -365,12 +368,17 @@ def should_update_localized_build_information?(options) end def reject_build_waiting_for_review(build) - waiting_for_review_build = build.app.get_builds(filter: { "betaAppReviewSubmission.betaReviewState" => "WAITING_FOR_REVIEW" }, includes: "betaAppReviewSubmission,preReleaseVersion").first + waiting_for_review_build = build.app.get_builds( + filter: { "betaAppReviewSubmission.betaReviewState" => "WAITING_FOR_REVIEW,IN_REVIEW", + "expired" => false, + "preReleaseVersion.version" => build.pre_release_version.version }, + includes: "betaAppReviewSubmission,preReleaseVersion" + ).first unless waiting_for_review_build.nil? UI.important("Another build is already in review. Going to remove that build and submit the new one.") - UI.important("Deleting beta app review submission for build: #{waiting_for_review_build.app_version} - #{waiting_for_review_build.version}") - waiting_for_review_build.beta_app_review_submission.delete! - UI.success("Deleted beta app review submission for previous build: #{waiting_for_review_build.app_version} - #{waiting_for_review_build.version}") + UI.important("Canceling beta app review submission for build: #{waiting_for_review_build.app_version} - #{waiting_for_review_build.version}") + waiting_for_review_build.expire! + UI.success("Canceled beta app review submission for previous build: #{waiting_for_review_build.app_version} - #{waiting_for_review_build.version}") end end @@ -383,6 +391,7 @@ def expire_previous_builds(build) end # If App Store Connect API token, use token. + # If api_key is specified and it is an Individual API Key, don't use token but use username. # If itc_provider was explicitly specified, use it. # If there are multiple teams, infer the provider from the selected team name. # If there are fewer than two teams, don't infer the provider. @@ -399,6 +408,14 @@ def transporter_for_selected_team(options) api_key end + # Currently no kind of transporters accept an Individual API Key. Use username and app-specific password instead. + # See https://github.com/fastlane/fastlane/issues/22115 + is_individual_key = !api_key.nil? && api_key[:issuer_id].nil? + if is_individual_key + api_key = nil + api_token = nil + end + unless api_token.nil? api_token.refresh! if api_token.expired? return FastlaneCore::ItunesTransporter.new(nil, nil, false, nil, api_token.text, altool_compatible_command: true, api_key: api_key) diff --git a/pilot/lib/pilot/manager.rb b/pilot/lib/pilot/manager.rb index 92c74c65251..9f19e69f3b0 100644 --- a/pilot/lib/pilot/manager.rb +++ b/pilot/lib/pilot/manager.rb @@ -85,8 +85,8 @@ def fetch_app_platform(required: true) result ||= FastlaneCore::IpaFileAnalyser.fetch_app_platform(config[:ipa]) if config[:ipa] result ||= FastlaneCore::PkgFileAnalyser.fetch_app_platform(config[:pkg]) if config[:pkg] if required - result ||= UI.input("Please enter the app's platform (appletvos, ios, osx): ") - UI.user_error!("App Platform must be ios, appletvos, or osx") unless ['ios', 'appletvos', 'osx'].include?(result) + result ||= UI.input("Please enter the app's platform (appletvos, ios, osx, xros): ") + UI.user_error!("App Platform must be ios, appletvos, osx, or xros") unless ['ios', 'appletvos', 'osx', 'xros'].include?(result) UI.verbose("App Platform (#{result})") end return result diff --git a/pilot/lib/pilot/options.rb b/pilot/lib/pilot/options.rb index 184b11a9002..2486dafcff7 100644 --- a/pilot/lib/pilot/options.rb +++ b/pilot/lib/pilot/options.rb @@ -49,7 +49,7 @@ def self.available_options description: "The platform to use (optional)", optional: true, verify_block: proc do |value| - UI.user_error!("The platform can only be ios, appletvos, or osx") unless ['ios', 'appletvos', 'osx'].include?(value) + UI.user_error!("The platform can only be ios, appletvos, osx, or xros") unless ['ios', 'appletvos', 'osx', 'xros'].include?(value) end), FastlaneCore::ConfigItem.new(key: :apple_id, short_option: "-p", diff --git a/pilot/spec/build_manager_spec.rb b/pilot/spec/build_manager_spec.rb index 0fb9842acea..83453e33cef 100644 --- a/pilot/spec/build_manager_spec.rb +++ b/pilot/spec/build_manager_spec.rb @@ -599,6 +599,53 @@ end describe "#upload" do + describe "shows the correct notices" do + let(:fake_build_manager) { Pilot::BuildManager.new } + let(:fake_app_id) { 123 } + let(:fake_dir) { "fake dir" } + let(:fake_app_platform) { "ios" } + let(:upload_options) do + { + apple_id: fake_app_id, + skip_waiting_for_build_processing: true, + changelog: "changelog contents", + ipa: 'foo' + } + end + + before(:each) do + allow(fake_build_manager).to receive(:login) + allow(fake_build_manager).to receive(:fetch_app_platform).and_return(fake_app_platform) + allow(Dir).to receive(:mktmpdir).and_return(fake_dir) + + fake_ipauploadpackagebuilder = double + allow(fake_ipauploadpackagebuilder).to receive(:generate).with(app_id: fake_app_id, ipa_path: upload_options[:ipa], package_path: fake_dir, platform: fake_app_platform).and_return(true) + allow(FastlaneCore::IpaUploadPackageBuilder).to receive(:new).and_return(fake_ipauploadpackagebuilder) + + fake_itunestransporter = double + allow(fake_itunestransporter).to receive(:upload).and_return(true) + allow(FastlaneCore::ItunesTransporter).to receive(:new).and_return(fake_itunestransporter) + + fake_build = double + expect(fake_build_manager).to receive(:wait_for_build_processing_to_be_complete).and_return(fake_build) + + expect(fake_build_manager).to receive(:distribute).with(upload_options, build: fake_build) + end + + it "does not advertise `skip_waiting_for_build_processing` if the option is set" do + expect(FastlaneCore::UI).to_not(receive(:message).with("If you want to skip waiting for the processing to be finished, use the `skip_waiting_for_build_processing` option")) + expect(FastlaneCore::UI).to_not(receive(:message).with("Note that if `skip_waiting_for_build_processing` is used but a `changelog` is supplied, this process will wait for the build to appear on App Store Connect, update the changelog and then skip the remaining of the processing steps.")) + + fake_build_manager.upload(upload_options) + end + + it "shows notice when using `skip_waiting_for_build_processing` and changelog together" do + expect(FastlaneCore::UI).to(receive(:important).with("`skip_waiting_for_build_processing` used and `changelog` supplied - will wait until build appears on App Store Connect, update the changelog and then skip the rest of the remaining of the processing steps.")) + + fake_build_manager.upload(upload_options) + end + end + describe "uses Manager.login (which does spaceship login) for ipa" do let(:fake_build_manager) { Pilot::BuildManager.new } let(:fake_app_id) { 123 } @@ -660,7 +707,7 @@ expect(fake_build).to receive(:app_version) expect(fake_build).to receive(:version) expect(UI).to receive(:message).with("If you want to skip waiting for the processing to be finished, use the `skip_waiting_for_build_processing` option") - expect(UI).to receive(:message).with("Note that if `skip_waiting_for_build_processing` is used but a `changelog` is supplied, this process will wait for the build to appear on AppStoreConnect, update the changelog and then skip the remaining of the processing steps.") + expect(UI).to receive(:message).with("Note that if `skip_waiting_for_build_processing` is used but a `changelog` is supplied, this process will wait for the build to appear on App Store Connect, update the changelog and then skip the remaining of the processing steps.") expect(FastlaneCore::BuildWatcher).to receive(:wait_for_build_processing_to_be_complete).and_return(fake_build) expect(fake_build_manager).to receive(:distribute).with(upload_options, build: fake_build) @@ -698,7 +745,7 @@ expect(fake_build).to receive(:app_version) expect(fake_build).to receive(:version) expect(UI).to receive(:message).with("If you want to skip waiting for the processing to be finished, use the `skip_waiting_for_build_processing` option") - expect(UI).to receive(:message).with("Note that if `skip_waiting_for_build_processing` is used but a `changelog` is supplied, this process will wait for the build to appear on AppStoreConnect, update the changelog and then skip the remaining of the processing steps.") + expect(UI).to receive(:message).with("Note that if `skip_waiting_for_build_processing` is used but a `changelog` is supplied, this process will wait for the build to appear on App Store Connect, update the changelog and then skip the remaining of the processing steps.") expect(FastlaneCore::BuildWatcher).to receive(:wait_for_build_processing_to_be_complete).and_return(fake_build) expect(fake_build_manager).to receive(:distribute).with(upload_options, build: fake_build) @@ -824,9 +871,12 @@ describe "#transporter_for_selected_team" do let(:fake_manager) { Pilot::BuildManager.new } - let(:fake_api_key_json_path) do + let(:fake_team_api_key_json_path) do "./spaceship/spec/connect_api/fixtures/asc_key.json" end + let(:fake_individual_api_key_json_path) do + "./spaceship/spec/connect_api/fixtures/asc_individual_key.json" + end let(:selected_team_id) { "123" } let(:selected_team_name) { "123 name" } @@ -843,9 +893,9 @@ } end - it "with API token" do + it "with Team API Key and API token" do options = {} - allow(Spaceship::ConnectAPI).to receive(:token).and_return(Spaceship::ConnectAPI::Token.from(filepath: fake_api_key_json_path)) + allow(Spaceship::ConnectAPI).to receive(:token).and_return(Spaceship::ConnectAPI::Token.from(filepath: fake_team_api_key_json_path)) transporter = fake_manager.send(:transporter_for_selected_team, options) expect(transporter.instance_variable_get(:@jwt)).not_to(be_nil) @@ -854,6 +904,17 @@ expect(transporter.instance_variable_get(:@provider_short_name)).to be_nil end + it "with Individual API Key" do + options = { username: "josh" } + allow(Spaceship::ConnectAPI).to receive(:token).and_return(Spaceship::ConnectAPI::Token.from(filepath: fake_individual_api_key_json_path)) + + transporter = fake_manager.send(:transporter_for_selected_team, options) + expect(transporter.instance_variable_get(:@jwt)).to(be_nil) + expect(transporter.instance_variable_get(:@user)).to eq("josh") + expect(transporter.instance_variable_get(:@password)).to eq("DELIVERPASS") + expect(transporter.instance_variable_get(:@provider_short_name)).to(be_nil) + end + describe "with itc_provider" do it "with nil Spaceship::TunesClient" do options = { username: "josh", itc_provider: "123456789" } diff --git a/pilot/spec/manager_spec.rb b/pilot/spec/manager_spec.rb index 6af5f91e481..21062701f89 100644 --- a/pilot/spec/manager_spec.rb +++ b/pilot/spec/manager_spec.rb @@ -520,7 +520,7 @@ end it "asks user to enter the app's platform manually" do - expect(UI).to receive(:input).with("Please enter the app's platform (appletvos, ios, osx): ").and_return(fake_app_platform) + expect(UI).to receive(:input).with("Please enter the app's platform (appletvos, ios, osx, xros): ").and_return(fake_app_platform) fetch_app_platform_result = fake_manager.fetch_app_platform @@ -542,7 +542,7 @@ end it "asks user to enter the app's platform manually" do - expect(UI).to receive(:input).with("Please enter the app's platform (appletvos, ios, osx): ").and_return(fake_app_platform) + expect(UI).to receive(:input).with("Please enter the app's platform (appletvos, ios, osx, xros): ").and_return(fake_app_platform) fetch_app_platform_result = fake_manager.fetch_app_platform @@ -566,7 +566,7 @@ end it "does not ask user to enter the app's platform manually" do - expect(UI).not_to receive(:input).with("Please enter the app's platform (appletvos, ios, osx): ") + expect(UI).not_to receive(:input).with("Please enter the app's platform (appletvos, ios, osx, xros): ") fetch_app_platform_result = fake_manager.fetch_app_platform(required: false) @@ -589,12 +589,12 @@ allow(UI) .to receive(:input) - .with("Please enter the app's platform (appletvos, ios, osx): ") + .with("Please enter the app's platform (appletvos, ios, osx, xros): ") .and_return(invalid_app_platform) end it "raises the 'invalid platform' exception" do - expect(UI).to receive(:user_error!).with("App Platform must be ios, appletvos, or osx") + expect(UI).to receive(:user_error!).with("App Platform must be ios, appletvos, osx, or xros") fake_manager.fetch_app_platform end diff --git a/scan/lib/scan/module.rb b/scan/lib/scan/module.rb index 4d214f8851c..cc5dee8f888 100644 --- a/scan/lib/scan/module.rb +++ b/scan/lib/scan/module.rb @@ -24,7 +24,7 @@ def scanfile_name def building_mac_catalyst_for_mac? return false unless Scan.project - Scan.project.supports_mac_catalyst? && Scan.config[:catalyst_platform] == "macos" + Scan.config[:catalyst_platform] == "macos" && Scan.project.supports_mac_catalyst? end end diff --git a/scan/lib/scan/options.rb b/scan/lib/scan/options.rb index 690f796f8ef..2e12a4fb242 100755 --- a/scan/lib/scan/options.rb +++ b/scan/lib/scan/options.rb @@ -537,7 +537,16 @@ def self.available_options env_name: "SCAN_FAIL_BUILD", description: "Should this step stop the build if the tests fail? Set this to false if you're using trainer", type: Boolean, - default_value: true) + default_value: true), + FastlaneCore::ConfigItem.new(key: :package_authorization_provider, + env_name: "SCAN_PACKAGE_AUTHORIZATION_PROVIDER", + description: "Lets xcodebuild use a specified package authorization provider (keychain|netrc)", + optional: true, + type: String, + verify_block: proc do |value| + av = %w(netrc keychain) + UI.user_error!("Unsupported authorization provider '#{value}', must be: #{av}") unless av.include?(value) + end) ] end diff --git a/scan/spec/detect_values_spec.rb b/scan/spec/detect_values_spec.rb index 212214c514c..d8917e65841 100644 --- a/scan/spec/detect_values_spec.rb +++ b/scan/spec/detect_values_spec.rb @@ -212,7 +212,7 @@ context "catalyst" do it "ios", requires_xcodebuild: true do options = { project: "./scan/examples/standard/app.xcodeproj" } - expect_any_instance_of(FastlaneCore::Project).to receive(:supports_mac_catalyst?).and_return(true) + expect_any_instance_of(FastlaneCore::Project).not_to receive(:supports_mac_catalyst?) Scan.config = FastlaneCore::Configuration.create(Scan::Options.available_options, options) expect(Scan.config[:destination].first).to match(/platform=iOS/) end diff --git a/sigh/spec/spec_helper.rb b/sigh/spec/spec_helper.rb index 52e944c0de3..fa8ae70ebe9 100644 --- a/sigh/spec/spec_helper.rb +++ b/sigh/spec/spec_helper.rb @@ -41,6 +41,7 @@ def sigh_stub_spaceship_connect(inhouse: false, create_profile_app_identifier: n profileContent: Base64.encode64("profile content") }) allow(profile).to receive(:bundle_id).and_return(bundle_id) + allow(profile).to receive(:expiration_date).and_return(Date.today.next_year.to_time.utc.strftime("%Y-%m-%dT%H:%M:%S%:z")) profile end @@ -62,6 +63,12 @@ def sigh_stub_spaceship_connect(inhouse: false, create_profile_app_identifier: n expect(profile).to receive(:delete!) if expect_delete + if valid_profiles + allow(profile).to receive(:expiration_date).and_return(Date.today.next_year.to_time.utc.strftime("%Y-%m-%dT%H:%M:%S%:z")) + else + allow(profile).to receive(:expiration_date).and_return(Date.today.prev_year.to_time.utc.strftime("%Y-%m-%dT%H:%M:%S%:z")) + end + profile end end diff --git a/snapshot/lib/snapshot/options.rb b/snapshot/lib/snapshot/options.rb index c78f052141e..daa956fde00 100644 --- a/snapshot/lib/snapshot/options.rb +++ b/snapshot/lib/snapshot/options.rb @@ -23,7 +23,7 @@ def self.plain_options short_option: "-w", env_name: "SNAPSHOT_WORKSPACE", optional: true, - description: "Path the workspace file", + description: "Path to the workspace file", verify_block: proc do |value| v = File.expand_path(value.to_s) UI.user_error!("Workspace file not found at path '#{v}'") unless File.exist?(v) @@ -34,7 +34,7 @@ def self.plain_options short_option: "-p", optional: true, env_name: "SNAPSHOT_PROJECT", - description: "Path the project file", + description: "Path to the project file", verify_block: proc do |value| v = File.expand_path(value.to_s) UI.user_error!("Project file not found at path '#{v}'") unless File.exist?(v) @@ -267,6 +267,15 @@ def self.plain_options description: "Prevents packages from automatically being resolved to versions other than those recorded in the `Package.resolved` file", type: Boolean, default_value: false), + FastlaneCore::ConfigItem.new(key: :package_authorization_provider, + env_name: "SNAPSHOT_PACKAGE_AUTHORIZATION_PROVIDER", + description: "Lets xcodebuild use a specified package authorization provider (keychain|netrc)", + optional: true, + type: String, + verify_block: proc do |value| + av = %w(netrc keychain) + UI.user_error!("Unsupported authorization provider '#{value}', must be: #{av}") unless av.include?(value) + end), FastlaneCore::ConfigItem.new(key: :testplan, env_name: "SNAPSHOT_TESTPLAN", description: "The testplan associated with the scheme that should be used for testing", diff --git a/snapshot/lib/snapshot/setup.rb b/snapshot/lib/snapshot/setup.rb index b7cb7c795c9..135808818af 100644 --- a/snapshot/lib/snapshot/setup.rb +++ b/snapshot/lib/snapshot/setup.rb @@ -34,7 +34,7 @@ def self.create(path, is_swift_fastfile: false, print_instructions_on_failure: f puts("✅ Successfully created #{snapshot_helper_filename} '#{File.join(path, snapshot_helper_filename)}'".green) puts("✅ Successfully created new Snapfile at '#{snapfile_path}'".green) puts("-------------------------------------------------------".yellow) - print_instructions(snapshot_helper_filename: snapshot_helper_filename, snapfile_path: snapfile_path) + print_instructions(snapshot_helper_filename: snapshot_helper_filename) end def self.print_instructions(snapshot_helper_filename: nil) diff --git a/spaceship/docs/AppStoreConnect.md b/spaceship/docs/AppStoreConnect.md index f9958c0b916..d5ba4b40c29 100644 --- a/spaceship/docs/AppStoreConnect.md +++ b/spaceship/docs/AppStoreConnect.md @@ -114,10 +114,10 @@ You can then go ahead and modify app metadata on the version objects: v = app.get_edit_app_store_version # Access information -v.app_store_state # => "Waiting for Review" +v.app_version_state # => "Waiting for Review" v.version_string # => "0.9.14" -# Build is not always available in all app_store_state, e.g. not available in `Prepare for Submission` +# Build is not always available in all app_version_state, e.g. not available in `Prepare for Submission` build_number = v.build.nil? ? nil : v.build.version # Update app metadata @@ -162,12 +162,12 @@ Available options: attr_accessor :platform attr_accessor :version_string attr_accessor :app_store_state +attr_accessor :app_version_state attr_accessor :store_icon attr_accessor :watch_store_icon attr_accessor :copyright attr_accessor :release_type attr_accessor :earliest_release_date -attr_accessor :uses_idfa attr_accessor :is_watch_only attr_accessor :downloadable attr_accessor :created_date @@ -220,7 +220,7 @@ version.select_build(build_id: build.id) ### Submit app for App Store Review ```ruby -# Check out submit_for_review.rb to get an overview how to modify idfa, submission information +# Check out submit_for_review.rb to get an overview how to modify submission information version.create_app_store_version_submission ``` diff --git a/spaceship/lib/spaceship/connect_api.rb b/spaceship/lib/spaceship/connect_api.rb index 4a3d09b9fee..6735a894af2 100644 --- a/spaceship/lib/spaceship/connect_api.rb +++ b/spaceship/lib/spaceship/connect_api.rb @@ -38,6 +38,8 @@ require 'spaceship/connect_api/models/custom_app_user' require 'spaceship/connect_api/models/pre_release_version' +require 'spaceship/connect_api/models/app_availability' +require 'spaceship/connect_api/models/territory_availability' require 'spaceship/connect_api/models/app_data_usage' require 'spaceship/connect_api/models/app_data_usage_category' require 'spaceship/connect_api/models/app_data_usage_data_protection' @@ -62,7 +64,6 @@ require 'spaceship/connect_api/models/app_store_version_localization' require 'spaceship/connect_api/models/app_store_version_phased_release' require 'spaceship/connect_api/models/app_store_version' -require 'spaceship/connect_api/models/idfa_declaration' require 'spaceship/connect_api/models/review_submission' require 'spaceship/connect_api/models/review_submission_item' require 'spaceship/connect_api/models/reset_ratings_request' @@ -86,9 +87,10 @@ module Platform IOS = "IOS" MAC_OS = "MAC_OS" TV_OS = "TV_OS" + VISION_OS = "VISION_OS" WATCH_OS = "WATCH_OS" - ALL = [IOS, MAC_OS, TV_OS, WATCH_OS] + ALL = [IOS, MAC_OS, TV_OS, VISION_OS, WATCH_OS] def self.map(platform) return platform if ALL.include?(platform) @@ -101,6 +103,8 @@ def self.map(platform) return Spaceship::ConnectAPI::Platform::MAC_OS when :ios return Spaceship::ConnectAPI::Platform::IOS + when :xros, :visionos + return Spaceship::ConnectAPI::Platform::VISION_OS else raise "Cannot find a matching platform for '#{platform}' - valid values are #{ALL.join(', ')}" end @@ -123,7 +127,7 @@ def self.map(platform) case platform.to_sym when :osx, :macos, :mac return Spaceship::ConnectAPI::Platform::MAC_OS - when :ios + when :ios, :xros, :visionos return Spaceship::ConnectAPI::Platform::IOS else raise "Cannot find a matching platform for '#{platform}' - valid values are #{ALL.join(', ')}" diff --git a/spaceship/lib/spaceship/connect_api/api_client.rb b/spaceship/lib/spaceship/connect_api/api_client.rb index e9494793bfd..c66efac4d96 100644 --- a/spaceship/lib/spaceship/connect_api/api_client.rb +++ b/spaceship/lib/spaceship/connect_api/api_client.rb @@ -69,7 +69,7 @@ def initialize(cookie: nil, current_team_id: nil, token: nil, csrf_tokens: nil, # Forwarding to class level if using web session. def hostname if @token - return "https://api.appstoreconnect.apple.com/v1/" + return "https://api.appstoreconnect.apple.com/" end return self.class.hostname end @@ -184,7 +184,7 @@ def with_asc_retry(tries = 5, backoff = 1, &_block) if tries.zero? raise error else - msg = "Token has expired or has been revoked! Trying to refresh..." + msg = "Token has expired, issued-at-time is in the future, or has been revoked! Trying to refresh..." puts(msg) if Spaceship::Globals.verbose? @token.refresh! retry diff --git a/spaceship/lib/spaceship/connect_api/models/app.rb b/spaceship/lib/spaceship/connect_api/models/app.rb index 1af5e1b1759..8dc0de5d5ee 100644 --- a/spaceship/lib/spaceship/connect_api/models/app.rb +++ b/spaceship/lib/spaceship/connect_api/models/app.rb @@ -112,34 +112,29 @@ def update(client: nil, attributes: nil, app_price_tier_id: nil, territory_ids: def fetch_live_app_info(client: nil, includes: Spaceship::ConnectAPI::AppInfo::ESSENTIAL_INCLUDES) client ||= Spaceship::ConnectAPI states = [ - Spaceship::ConnectAPI::AppInfo::AppStoreState::READY_FOR_SALE, - Spaceship::ConnectAPI::AppInfo::AppStoreState::PENDING_APPLE_RELEASE, - Spaceship::ConnectAPI::AppInfo::AppStoreState::PENDING_DEVELOPER_RELEASE, - Spaceship::ConnectAPI::AppInfo::AppStoreState::PROCESSING_FOR_APP_STORE, - Spaceship::ConnectAPI::AppInfo::AppStoreState::IN_REVIEW, - Spaceship::ConnectAPI::AppInfo::AppStoreState::DEVELOPER_REMOVED_FROM_SALE + Spaceship::ConnectAPI::AppInfo::State::READY_FOR_DISTRIBUTION, + Spaceship::ConnectAPI::AppInfo::State::PENDING_RELEASE, + Spaceship::ConnectAPI::AppInfo::State::IN_REVIEW ] resp = client.get_app_infos(app_id: id, includes: includes) return resp.to_models.select do |model| - states.include?(model.app_store_state) + states.include?(model.state) end.first end def fetch_edit_app_info(client: nil, includes: Spaceship::ConnectAPI::AppInfo::ESSENTIAL_INCLUDES) client ||= Spaceship::ConnectAPI states = [ - Spaceship::ConnectAPI::AppInfo::AppStoreState::PREPARE_FOR_SUBMISSION, - Spaceship::ConnectAPI::AppInfo::AppStoreState::DEVELOPER_REJECTED, - Spaceship::ConnectAPI::AppInfo::AppStoreState::REJECTED, - Spaceship::ConnectAPI::AppInfo::AppStoreState::METADATA_REJECTED, - Spaceship::ConnectAPI::AppInfo::AppStoreState::WAITING_FOR_REVIEW, - Spaceship::ConnectAPI::AppInfo::AppStoreState::INVALID_BINARY + Spaceship::ConnectAPI::AppInfo::State::PREPARE_FOR_SUBMISSION, + Spaceship::ConnectAPI::AppInfo::State::DEVELOPER_REJECTED, + Spaceship::ConnectAPI::AppInfo::State::REJECTED, + Spaceship::ConnectAPI::AppInfo::State::WAITING_FOR_REVIEW ] resp = client.get_app_infos(app_id: id, includes: includes) return resp.to_models.select do |model| - states.include?(model.app_store_state) + states.include?(model.state) end.first end @@ -149,6 +144,16 @@ def fetch_latest_app_info(client: nil, includes: Spaceship::ConnectAPI::AppInfo: return resp.to_models.first end + # + # App Availabilities + # + + def get_app_availabilities(client: nil, filter: {}, includes: "territoryAvailabilities", limit: { "territoryAvailabilities": 200 }) + client ||= Spaceship::ConnectAPI + resp = client.get_app_availabilities(app_id: id, filter: filter, includes: includes, limit: limit, sort: nil) + return resp.to_models.first + end + # # Available Territories # @@ -178,11 +183,11 @@ def reject_version_if_possible!(client: nil, platform: nil) client ||= Spaceship::ConnectAPI platform ||= Spaceship::ConnectAPI::Platform::IOS filter = { - appStoreState: [ - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::PENDING_APPLE_RELEASE, - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::PENDING_DEVELOPER_RELEASE, - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::IN_REVIEW, - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::WAITING_FOR_REVIEW + appVersionState: [ + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::PENDING_APPLE_RELEASE, + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::PENDING_DEVELOPER_RELEASE, + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::IN_REVIEW, + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::WAITING_FOR_REVIEW ].join(","), platform: platform } @@ -236,9 +241,9 @@ def get_live_app_store_version(client: nil, platform: nil, includes: Spaceship:: client ||= Spaceship::ConnectAPI platform ||= Spaceship::ConnectAPI::Platform::IOS filter = { - appStoreState: [ - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::READY_FOR_SALE, - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::DEVELOPER_REMOVED_FROM_SALE + appVersionState: [ + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::READY_FOR_DISTRIBUTION, + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::PROCESSING_FOR_DISTRIBUTION ].join(","), platform: platform } @@ -249,13 +254,13 @@ def get_edit_app_store_version(client: nil, platform: nil, includes: Spaceship:: client ||= Spaceship::ConnectAPI platform ||= Spaceship::ConnectAPI::Platform::IOS filter = { - appStoreState: [ - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::PREPARE_FOR_SUBMISSION, - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::DEVELOPER_REJECTED, - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::REJECTED, - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::METADATA_REJECTED, - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::WAITING_FOR_REVIEW, - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::INVALID_BINARY + appVersionState: [ + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::PREPARE_FOR_SUBMISSION, + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::DEVELOPER_REJECTED, + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::REJECTED, + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::METADATA_REJECTED, + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::WAITING_FOR_REVIEW, + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::INVALID_BINARY ].join(","), platform: platform } @@ -270,7 +275,7 @@ def get_in_review_app_store_version(client: nil, platform: nil, includes: Spaces client ||= Spaceship::ConnectAPI platform ||= Spaceship::ConnectAPI::Platform::IOS filter = { - appStoreState: Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::IN_REVIEW, + appVersionState: Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::IN_REVIEW, platform: platform } return get_app_store_versions(client: client, filter: filter, includes: includes).first @@ -280,9 +285,9 @@ def get_pending_release_app_store_version(client: nil, platform: nil, includes: client ||= Spaceship::ConnectAPI platform ||= Spaceship::ConnectAPI::Platform::IOS filter = { - appStoreState: [ - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::PENDING_APPLE_RELEASE, - Spaceship::ConnectAPI::AppStoreVersion::AppStoreState::PENDING_DEVELOPER_RELEASE + appVersionState: [ + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::PENDING_APPLE_RELEASE, + Spaceship::ConnectAPI::AppStoreVersion::AppVersionState::PENDING_DEVELOPER_RELEASE ].join(','), platform: platform } diff --git a/spaceship/lib/spaceship/connect_api/models/app_availability.rb b/spaceship/lib/spaceship/connect_api/models/app_availability.rb new file mode 100644 index 00000000000..542d152abbd --- /dev/null +++ b/spaceship/lib/spaceship/connect_api/models/app_availability.rb @@ -0,0 +1,23 @@ +require_relative '../model' +module Spaceship + class ConnectAPI + class AppAvailability + include Spaceship::ConnectAPI::Model + + attr_accessor :app + attr_accessor :available_in_new_territories + + attr_accessor :territoryAvailabilities + + attr_mapping({ + app: 'app', + availableInNewTerritories: 'available_in_new_territories', + territoryAvailabilities: 'territory_availabilities' + }) + + def self.type + return 'appAvailabilities' + end + end + end +end diff --git a/spaceship/lib/spaceship/connect_api/models/app_info.rb b/spaceship/lib/spaceship/connect_api/models/app_info.rb index 3d2900716af..1f2b603532c 100644 --- a/spaceship/lib/spaceship/connect_api/models/app_info.rb +++ b/spaceship/lib/spaceship/connect_api/models/app_info.rb @@ -5,6 +5,7 @@ class AppInfo include Spaceship::ConnectAPI::Model attr_accessor :app_store_state + attr_accessor :state attr_accessor :app_store_age_rating attr_accessor :brazil_age_rating attr_accessor :kids_age_band @@ -16,6 +17,7 @@ class AppInfo attr_accessor :secondary_subcategory_one attr_accessor :secondary_subcategory_two + # Deprecated in App Store Connect API specification 3.3 module AppStoreState ACCEPTED = "ACCEPTED" DEVELOPER_REJECTED = "DEVELOPER_REJECTED" @@ -36,6 +38,20 @@ module AppStoreState REPLACED_WITH_NEW_VERSION = "REPLACED_WITH_NEW_VERSION" WAITING_FOR_EXPORT_COMPLIANCE = "WAITING_FOR_EXPORT_COMPLIANCE" WAITING_FOR_REVIEW = "WAITING_FOR_REVIEW" + NOT_APPLICABLE = "NOT_APPLICABLE" + end + + module State + ACCEPTED = "ACCEPTED" + DEVELOPER_REJECTED = "DEVELOPER_REJECTED" + IN_REVIEW = "IN_REVIEW" + PENDING_RELEASE = "PENDING_RELEASE" + PREPARE_FOR_SUBMISSION = "PREPARE_FOR_SUBMISSION" + READY_FOR_DISTRIBUTION = "READY_FOR_DISTRIBUTION" + READY_FOR_REVIEW = "READY_FOR_REVIEW" + REJECTED = "REJECTED" + REPLACED_WITH_NEW_INFO = "REPLACED_WITH_NEW_INFO" + WAITING_FOR_REVIEW = "WAITING_FOR_REVIEW" end module AppStoreAgeRating @@ -44,6 +60,7 @@ module AppStoreAgeRating attr_mapping({ "appStoreState" => "app_store_state", + "state" => "state", "appStoreAgeRating" => "app_store_age_rating", "brazilAgeRating" => "brazil_age_rating", "kidsAgeBand" => "kids_age_band", diff --git a/spaceship/lib/spaceship/connect_api/models/app_store_version.rb b/spaceship/lib/spaceship/connect_api/models/app_store_version.rb index f38a71eb72a..16d0269c0f6 100644 --- a/spaceship/lib/spaceship/connect_api/models/app_store_version.rb +++ b/spaceship/lib/spaceship/connect_api/models/app_store_version.rb @@ -10,35 +10,62 @@ class AppStoreVersion attr_accessor :platform attr_accessor :version_string attr_accessor :app_store_state + attr_accessor :app_version_state attr_accessor :store_icon attr_accessor :watch_store_icon attr_accessor :copyright attr_accessor :release_type attr_accessor :earliest_release_date # 2020-06-17T12:00:00-07:00 - attr_accessor :uses_idfa attr_accessor :is_watch_only attr_accessor :downloadable attr_accessor :created_date + attr_accessor :review_type attr_accessor :app_store_version_submission attr_accessor :app_store_version_phased_release attr_accessor :app_store_review_detail attr_accessor :app_store_version_localizations + # Deprecated in App Store Connect API specification 3.3 module AppStoreState - READY_FOR_SALE = "READY_FOR_SALE" - READY_FOR_REVIEW = "READY_FOR_REVIEW" - PROCESSING_FOR_APP_STORE = "PROCESSING_FOR_APP_STORE" - PENDING_DEVELOPER_RELEASE = "PENDING_DEVELOPER_RELEASE" - PENDING_APPLE_RELEASE = "PENDING_APPLE_RELEASE" - IN_REVIEW = "IN_REVIEW" - WAITING_FOR_REVIEW = "WAITING_FOR_REVIEW" + ACCEPTED = "ACCEPTED" DEVELOPER_REJECTED = "DEVELOPER_REJECTED" DEVELOPER_REMOVED_FROM_SALE = "DEVELOPER_REMOVED_FROM_SALE" - REJECTED = "REJECTED" - PREPARE_FOR_SUBMISSION = "PREPARE_FOR_SUBMISSION" + IN_REVIEW = "IN_REVIEW" + INVALID_BINARY = "INVALID_BINARY" METADATA_REJECTED = "METADATA_REJECTED" + PENDING_APPLE_RELEASE = "PENDING_APPLE_RELEASE" + PENDING_CONTRACT = "PENDING_CONTRACT" + PENDING_DEVELOPER_RELEASE = "PENDING_DEVELOPER_RELEASE" + PREORDER_READY_FOR_SALE = "PREORDER_READY_FOR_SALE" + PREPARE_FOR_SUBMISSION = "PREPARE_FOR_SUBMISSION" + PROCESSING_FOR_APP_STORE = "PROCESSING_FOR_APP_STORE" + READY_FOR_REVIEW = "READY_FOR_REVIEW" + READY_FOR_SALE = "READY_FOR_SALE" + REJECTED = "REJECTED" + REMOVED_FROM_SALE = "REMOVED_FROM_SALE" + REPLACED_WITH_NEW_VERSION = "REPLACED_WITH_NEW_VERSION" + WAITING_FOR_EXPORT_COMPLIANCE = "WAITING_FOR_EXPORT_COMPLIANCE" + WAITING_FOR_REVIEW = "WAITING_FOR_REVIEW" + NOT_APPLICABLE = "NOT_APPLICABLE" + end + + module AppVersionState + ACCEPTED = "ACCEPTED" + DEVELOPER_REJECTED = "DEVELOPER_REJECTED" + IN_REVIEW = "IN_REVIEW" INVALID_BINARY = "INVALID_BINARY" + METADATA_REJECTED = "METADATA_REJECTED" + PENDING_APPLE_RELEASE = "PENDING_APPLE_RELEASE" + PENDING_DEVELOPER_RELEASE = "PENDING_DEVELOPER_RELEASE" + PREPARE_FOR_SUBMISSION = "PREPARE_FOR_SUBMISSION" + PROCESSING_FOR_DISTRIBUTION = "PROCESSING_FOR_DISTRIBUTION" + READY_FOR_DISTRIBUTION = "READY_FOR_DISTRIBUTION" + READY_FOR_REVIEW = "READY_FOR_REVIEW" + REJECTED = "REJECTED" + REPLACED_WITH_NEW_VERSION = "REPLACED_WITH_NEW_VERSION" + WAITING_FOR_EXPORT_COMPLIANCE = "WAITING_FOR_EXPORT_COMPLIANCE" + WAITING_FOR_REVIEW = "WAITING_FOR_REVIEW" end module ReleaseType @@ -47,19 +74,25 @@ module ReleaseType SCHEDULED = "SCHEDULED" end + module ReviewType + APP_STORE = "APP_STORE" + NOTARIZATION = "NOTARIZATION" + end + attr_mapping({ "platform" => "platform", "versionString" => "version_string", "appStoreState" => "app_store_state", + "appVersionState" => "app_version_state", "storeIcon" => "store_icon", "watchStoreIcon" => "watch_store_icon", "copyright" => "copyright", "releaseType" => "release_type", "earliestReleaseDate" => "earliest_release_date", - "usesIdfa" => "uses_idfa", "isWatchOnly" => "is_watch_only", "downloadable" => "downloadable", "createdDate" => "created_date", + "reviewType" => "review_type", "appStoreVersionSubmission" => "app_store_version_submission", "build" => "build", @@ -92,7 +125,7 @@ def reject! # API # - # app,routingAppCoverage,resetRatingsRequest,appStoreVersionSubmission,appStoreVersionPhasedRelease,ageRatingDeclaration,appStoreReviewDetail,idfaDeclaration,gameCenterConfiguration + # app,routingAppCoverage,resetRatingsRequest,appStoreVersionSubmission,appStoreVersionPhasedRelease,ageRatingDeclaration,appStoreReviewDetail,gameCenterConfiguration def self.get(client: nil, app_store_version_id: nil, includes: nil, limit: nil, sort: nil) client ||= Spaceship::ConnectAPI return client.get_app_store_version( @@ -206,22 +239,6 @@ def select_build(client: nil, build_id: nil) return resp.to_models.first end - # - # IDFA Declarations - # - - def fetch_idfa_declaration(client: nil) - client ||= Spaceship::ConnectAPI - resp = client.get_idfa_declaration(app_store_version_id: id) - return resp.to_models.first - end - - def create_idfa_declaration(client: nil, attributes: nil) - client ||= Spaceship::ConnectAPI - resp = client.post_idfa_declaration(app_store_version_id: id, attributes: attributes) - return resp.to_models.first - end - # # Reset Ratings Requests # diff --git a/spaceship/lib/spaceship/connect_api/models/beta_tester.rb b/spaceship/lib/spaceship/connect_api/models/beta_tester.rb index ccf34a52c11..a90750afb65 100644 --- a/spaceship/lib/spaceship/connect_api/models/beta_tester.rb +++ b/spaceship/lib/spaceship/connect_api/models/beta_tester.rb @@ -8,7 +8,21 @@ class BetaTester attr_accessor :last_name attr_accessor :email attr_accessor :invite_type - attr_accessor :invitation + attr_accessor :beta_tester_state + attr_accessor :is_deleted + attr_accessor :last_modified_date + attr_accessor :installed_cf_bundle_short_version_string + attr_accessor :installed_cf_bundle_version + attr_accessor :remove_after_date + attr_accessor :installed_device + attr_accessor :installed_os_version + attr_accessor :number_of_installed_devices + attr_accessor :latest_expiring_cf_bundle_short_version_string + attr_accessor :latest_expiring_cf_bundle_version_string + attr_accessor :installed_device_platform + attr_accessor :latest_installed_device + attr_accessor :latest_installed_os_version + attr_accessor :latest_installed_device_platform attr_accessor :apps attr_accessor :beta_groups @@ -20,7 +34,21 @@ class BetaTester "lastName" => "last_name", "email" => "email", "inviteType" => "invite_type", - "invitation" => "invitation", + "betaTesterState" => "beta_tester_state", + "isDeleted" => "is_deleted", + "lastModifiedDate" => "last_modified_date", + "installedCfBundleShortVersionString" => "installed_cf_bundle_short_version_string", + "installedCfBundleVersion" => "installed_cf_bundle_version", + "removeAfterDate" => "remove_after_date", + "installedDevice" => "installed_device", + "installedOsVersion" => "installed_os_version", + "numberOfInstalledDevices" => "number_of_installed_devices", + "latestExpiringCfBundleShortVersionString" => "latest_expiring_cf_bundle_short_version_string", + "latestExpiringCfBundleVersionString" => "latest_expiring_cf_bundle_version_string", + "installedDevicePlatform" => "installed_device_platform", + "latestInstalledDevice" => "latest_installed_device", + "latestInstalledOsVersion" => "latest_installed_os_version", + "latestInstalledDevicePlatform" => "latest_installed_device_platform", "apps" => "apps", "betaGroups" => "beta_groups", diff --git a/spaceship/lib/spaceship/connect_api/models/certificate.rb b/spaceship/lib/spaceship/connect_api/models/certificate.rb index dfeaa44f0d2..13630000ae0 100644 --- a/spaceship/lib/spaceship/connect_api/models/certificate.rb +++ b/spaceship/lib/spaceship/connect_api/models/certificate.rb @@ -55,7 +55,7 @@ def valid? Time.parse(expiration_date) > Time.now end - # Create a new code signing request that can be used to + # Create a new cert signing request that can be used to # generate a new certificate # @example # Create a new certificate signing request @@ -71,7 +71,7 @@ def self.create_certificate_signing_request ['CN', 'PEM', OpenSSL::ASN1::UTF8STRING] ]) csr.public_key = key.public_key - csr.sign(key, OpenSSL::Digest::SHA1.new) + csr.sign(key, OpenSSL::Digest::SHA256.new) return [csr, key] end diff --git a/spaceship/lib/spaceship/connect_api/models/device.rb b/spaceship/lib/spaceship/connect_api/models/device.rb index 49a816bbd6c..645b79e36b7 100644 --- a/spaceship/lib/spaceship/connect_api/models/device.rb +++ b/spaceship/lib/spaceship/connect_api/models/device.rb @@ -30,9 +30,11 @@ module DeviceClass IPOD = "IPOD" APPLE_TV = "APPLE_TV" MAC = "MAC" + APPLE_VISION_PRO = "APPLE_VISION_PRO" - # As of 2022-11-12, this is not officially supported by App Store Connect API + # As of 2024-03-08, this is not _officially_ supported by App Store Connect API (according to API docs)—yet still used in the API responses APPLE_SILICON_MAC = "APPLE_SILICON_MAC" + INTEL_MAC = "INTEL_MAC" end module Status @@ -68,7 +70,7 @@ def self.devices_for_platform(platform: nil, include_mac_in_profiles: false, cli device_platform = case platform when :osx, :macos, :mac Spaceship::ConnectAPI::Platform::MAC_OS - when :ios + when :ios, :tvos, :xros, :visionos Spaceship::ConnectAPI::Platform::IOS when :catalyst Spaceship::ConnectAPI::Platform::MAC_OS @@ -86,7 +88,8 @@ def self.devices_for_platform(platform: nil, include_mac_in_profiles: false, cli Spaceship::ConnectAPI::Device::DeviceClass::IPAD, Spaceship::ConnectAPI::Device::DeviceClass::IPHONE, Spaceship::ConnectAPI::Device::DeviceClass::IPOD, - Spaceship::ConnectAPI::Device::DeviceClass::APPLE_WATCH + Spaceship::ConnectAPI::Device::DeviceClass::APPLE_WATCH, + Spaceship::ConnectAPI::Device::DeviceClass::APPLE_VISION_PRO ] when :tvos [ @@ -94,7 +97,9 @@ def self.devices_for_platform(platform: nil, include_mac_in_profiles: false, cli ] when :macos, :catalyst [ - Spaceship::ConnectAPI::Device::DeviceClass::MAC + Spaceship::ConnectAPI::Device::DeviceClass::MAC, + Spaceship::ConnectAPI::Device::DeviceClass::APPLE_SILICON_MAC, + Spaceship::ConnectAPI::Device::DeviceClass::INTEL_MAC ] else [] @@ -106,9 +111,9 @@ def self.devices_for_platform(platform: nil, include_mac_in_profiles: false, cli end filter = { - status: Spaceship::ConnectAPI::Device::Status::ENABLED, - platform: device_platforms.uniq.join(',') + status: Spaceship::ConnectAPI::Device::Status::ENABLED } + filter[:platform] = device_platforms.uniq.join(',') unless device_platforms.empty? devices = Spaceship::ConnectAPI::Device.all( client: client, diff --git a/spaceship/lib/spaceship/connect_api/models/idfa_declaration.rb b/spaceship/lib/spaceship/connect_api/models/idfa_declaration.rb deleted file mode 100644 index 76888cb1ef1..00000000000 --- a/spaceship/lib/spaceship/connect_api/models/idfa_declaration.rb +++ /dev/null @@ -1,43 +0,0 @@ -require_relative '../model' -module Spaceship - class ConnectAPI - class IdfaDeclaration - include Spaceship::ConnectAPI::Model - - attr_accessor :serves_ads - attr_accessor :attributes_app_installation_to_previous_ad - attr_accessor :attributes_action_with_previous_ad - attr_accessor :honors_limited_ad_tracking - - module AppStoreAgeRating - FOUR_PLUS = "FOUR_PLUS" - end - - attr_mapping({ - "servesAds" => "serves_ads", - "attributesAppInstallationToPreviousAd" => "attributes_app_installation_to_previous_ad", - "attributesActionWithPreviousAd" => "attributes_action_with_previous_ad", - "honorsLimitedAdTracking" => "honors_limited_ad_tracking" - }) - - def self.type - return "idfaDeclarations" - end - - # - # API - # - - def update(client: nil, attributes: nil) - client ||= Spaceship::ConnectAPI - attributes = reverse_attr_mapping(attributes) - client.patch_idfa_declaration(idfa_declaration_id: id, attributes: attributes) - end - - def delete!(client: nil) - client ||= Spaceship::ConnectAPI - client.delete_idfa_declaration(idfa_declaration_id: id) - end - end - end -end diff --git a/spaceship/lib/spaceship/connect_api/models/profile.rb b/spaceship/lib/spaceship/connect_api/models/profile.rb index 59d661bc611..3e5da67ad9d 100644 --- a/spaceship/lib/spaceship/connect_api/models/profile.rb +++ b/spaceship/lib/spaceship/connect_api/models/profile.rb @@ -64,7 +64,14 @@ def self.type end def valid? - return profile_state == ProfileState::ACTIVE + # Provisioning profiles are not invalidated automatically on the dev portal when the certificate expires. + # They become Invalid only when opened directly in the portal 🤷. + # We need to do an extra check on the expiration date to ensure the profile is valid. + expired = Time.now.utc > Time.parse(self.expiration_date) + + is_valid = profile_state == ProfileState::ACTIVE && !expired + + return is_valid end # diff --git a/spaceship/lib/spaceship/connect_api/models/territory_availability.rb b/spaceship/lib/spaceship/connect_api/models/territory_availability.rb new file mode 100644 index 00000000000..aa0d96eb7e3 --- /dev/null +++ b/spaceship/lib/spaceship/connect_api/models/territory_availability.rb @@ -0,0 +1,62 @@ +require_relative '../model' +module Spaceship + class ConnectAPI + class TerritoryAvailability + include Spaceship::ConnectAPI::Model + + attr_accessor :available + attr_accessor :content_statuses + attr_accessor :pre_order_enabled + attr_accessor :pre_order_publish_date + attr_accessor :release_date + + module ContentStatus + AVAILABLE = "AVAILABLE" + AVAILABLE_FOR_PREORDER_ON_DATE = "AVAILABLE_FOR_PREORDER_ON_DATE" + PROCESSING_TO_NOT_AVAILABLE = "PROCESSING_TO_NOT_AVAILABLE" + PROCESSING_TO_AVAILABLE = "PROCESSING_TO_AVAILABLE" + PROCESSING_TO_PRE_ORDER = "PROCESSING_TO_PRE_ORDER" + AVAILABLE_FOR_SALE_UNRELEASED_APP = "AVAILABLE_FOR_SALE_UNRELEASED_APP" + PREORDER_ON_UNRELEASED_APP = "PREORDER_ON_UNRELEASED_APP" + AVAILABLE_FOR_PREORDER = "AVAILABLE_FOR_PREORDER" + MISSING_RATING = "MISSING_RATING" + CANNOT_SELL_RESTRICTED_RATING = "CANNOT_SELL_RESTRICTED_RATING" + BRAZIL_REQUIRED_TAX_ID = "BRAZIL_REQUIRED_TAX_ID" + MISSING_GRN = "MISSING_GRN" + UNVERIFIED_GRN = "UNVERIFIED_GRN" + CANNOT_SELL_SEVENTEEN_PLUS_APPS = "CANNOT_SELL_SEVENTEEN_PLUS_APPS" + CANNOT_SELL_SEXUALLY_EXPLICIT = "CANNOT_SELL_SEXUALLY_EXPLICIT" + CANNOT_SELL_NON_IOS_GAMES = "CANNOT_SELL_NON_IOS_GAMES" + CANNOT_SELL_SEVENTEEN_PLUS_GAMES = "CANNOT_SELL_SEVENTEEN_PLUS_GAMES" + CANNOT_SELL_FREQUENT_INTENSE_GAMBLING = "CANNOT_SELL_FREQUENT_INTENSE_GAMBLING" + CANNOT_SELL_CASINO = "CANNOT_SELL_CASINO" + CANNOT_SELL_CASINO_WITHOUT_GRAC = "CANNOT_SELL_CASINO_WITHOUT_GRAC" + CANNOT_SELL_CASINO_WITHOUT_AGE_VERIFICATION = "CANNOT_SELL_CASINO_WITHOUT_AGE_VERIFICATION" + CANNOT_SELL_FREQUENT_INTENSE_ALCOHOL_TOBACCO_DRUGS = "CANNOT_SELL_FREQUENT_INTENSE_ALCOHOL_TOBACCO_DRUGS" + CANNOT_SELL_FREQUENT_INTENSE_VIOLENCE = "CANNOT_SELL_FREQUENT_INTENSE_VIOLENCE" + CANNOT_SELL_FREQUENT_INTENSE_SEXUAL_CONTENT_NUDITY = "CANNOT_SELL_FREQUENT_INTENSE_SEXUAL_CONTENT_NUDITY" + CANNOT_SELL_INFREQUENT_MILD_ALCOHOL_TOBACCO_DRUGS = "CANNOT_SELL_INFREQUENT_MILD_ALCOHOL_TOBACCO_DRUGS" + CANNOT_SELL_INFREQUENT_MILD_SEXUAL_CONTENT_NUDITY = "CANNOT_SELL_INFREQUENT_MILD_SEXUAL_CONTENT_NUDITY" + CANNOT_SELL_ADULT_ONLY = "CANNOT_SELL_ADULT_ONLY" + CANNOT_SELL_FREQUENT_INTENSE = "CANNOT_SELL_FREQUENT_INTENSE" + CANNOT_SELL_FREQUENT_INTENSE_WITHOUT_GRAC = "CANNOT_SELL_FREQUENT_INTENSE_WITHOUT_GRAC" + CANNOT_SELL_GAMBLING_CONTESTS = "CANNOT_SELL_GAMBLING_CONTESTS" + CANNOT_SELL_GAMBLING = "CANNOT_SELL_GAMBLING" + CANNOT_SELL_CONTESTS = "CANNOT_SELL_CONTESTS" + CANNOT_SELL = "CANNOT_SELL" + end + + attr_mapping({ + available: 'available', + contentStatuses: 'content_statuses', + preOrderEnabled: 'pre_order_enabled', + preOrderPublishDate: 'pre_order_publish_date', + releaseDate: 'release_date' + }) + + def self.type + return 'territoryAvailabilities' + end + end + end +end diff --git a/spaceship/lib/spaceship/connect_api/provisioning/client.rb b/spaceship/lib/spaceship/connect_api/provisioning/client.rb index e6791bc953b..d7d886146f2 100644 --- a/spaceship/lib/spaceship/connect_api/provisioning/client.rb +++ b/spaceship/lib/spaceship/connect_api/provisioning/client.rb @@ -16,7 +16,7 @@ def initialize(cookie: nil, current_team_id: nil, token: nil, another_client: ni end def self.hostname - 'https://developer.apple.com/services-account/v1/' + 'https://developer.apple.com/services-account/' end # diff --git a/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb b/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb index e548c14efe4..d890464b58a 100644 --- a/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb +++ b/spaceship/lib/spaceship/connect_api/provisioning/provisioning.rb @@ -4,6 +4,10 @@ module Spaceship class ConnectAPI module Provisioning module API + module Version + V1 = "v1" + end + def provisioning_request_client=(provisioning_request_client) @provisioning_request_client = provisioning_request_client end @@ -19,12 +23,12 @@ def provisioning_request_client def get_bundle_ids(filter: {}, includes: nil, fields: nil, limit: nil, sort: nil) params = provisioning_request_client.build_params(filter: filter, includes: includes, fields: fields, limit: limit, sort: sort) - provisioning_request_client.get("bundleIds", params) + provisioning_request_client.get("#{Version::V1}/bundleIds", params) end def get_bundle_id(bundle_id_id: {}, includes: nil) params = provisioning_request_client.build_params(filter: nil, includes: includes, limit: nil, sort: nil) - provisioning_request_client.get("bundleIds/#{bundle_id_id}", params) + provisioning_request_client.get("#{Version::V1}/bundleIds/#{bundle_id_id}", params) end def post_bundle_id(name:, platform:, identifier:, seed_id:) @@ -42,7 +46,7 @@ def post_bundle_id(name:, platform:, identifier:, seed_id:) } } - provisioning_request_client.post("bundleIds", body) + provisioning_request_client.post("#{Version::V1}/bundleIds", body) end # @@ -51,12 +55,12 @@ def post_bundle_id(name:, platform:, identifier:, seed_id:) def get_bundle_id_capabilities(bundle_id_id:, includes: nil, limit: nil, sort: nil) params = provisioning_request_client.build_params(filter: nil, includes: includes, limit: limit, sort: sort) - provisioning_request_client.get("bundleIds/#{bundle_id_id}/bundleIdCapabilities", params) + provisioning_request_client.get("#{Version::V1}/bundleIds/#{bundle_id_id}/bundleIdCapabilities", params) end def get_available_bundle_id_capabilities(bundle_id_id:) params = provisioning_request_client.build_params(filter: { bundleId: bundle_id_id }) - provisioning_request_client.get("capabilities", params) + provisioning_request_client.get("#{Version::V1}/capabilities", params) end def post_bundle_id_capability(bundle_id_id:, capability_type:, settings: []) @@ -83,7 +87,7 @@ def post_bundle_id_capability(bundle_id_id:, capability_type:, settings: []) } } } - provisioning_request_client.post("bundleIdCapabilities", body) + provisioning_request_client.post("#{Version::V1}/bundleIdCapabilities", body) end def patch_bundle_id_capability(bundle_id_id:, seed_id:, enabled: false, capability_type:, settings: []) @@ -92,7 +96,12 @@ def patch_bundle_id_capability(bundle_id_id:, seed_id:, enabled: false, capabili type: "bundleIds", id: bundle_id_id, attributes: { - teamId: seed_id + permissions: { + edit: true, + delete: true + }, + seedId: seed_id, + teamId: provisioning_request_client.team_id }, relationships: { bundleIdCapabilities: { @@ -118,11 +127,11 @@ def patch_bundle_id_capability(bundle_id_id:, seed_id:, enabled: false, capabili } } - provisioning_request_client.patch("bundleIds/#{bundle_id_id}", body) + provisioning_request_client.patch("#{Version::V1}/bundleIds/#{bundle_id_id}", body) end def delete_bundle_id_capability(bundle_id_capability_id:) - provisioning_request_client.delete("bundleIdCapabilities/#{bundle_id_capability_id}") + provisioning_request_client.delete("#{Version::V1}/bundleIdCapabilities/#{bundle_id_capability_id}") end # @@ -132,15 +141,15 @@ def delete_bundle_id_capability(bundle_id_capability_id:) def get_certificates(profile_id: nil, filter: {}, includes: nil, fields: nil, limit: nil, sort: nil) params = provisioning_request_client.build_params(filter: filter, includes: includes, fields: fields, limit: limit, sort: sort) if profile_id.nil? - provisioning_request_client.get("certificates", params) + provisioning_request_client.get("#{Version::V1}/certificates", params) else - provisioning_request_client.get("profiles/#{profile_id}/certificates", params) + provisioning_request_client.get("#{Version::V1}/profiles/#{profile_id}/certificates", params) end end def get_certificate(certificate_id: nil, includes: nil) params = provisioning_request_client.build_params(filter: nil, includes: includes, limit: nil, sort: nil) - provisioning_request_client.get("certificates/#{certificate_id}", params) + provisioning_request_client.get("#{Version::V1}/certificates/#{certificate_id}", params) end def post_certificate(attributes: {}) @@ -151,13 +160,13 @@ def post_certificate(attributes: {}) } } - provisioning_request_client.post("certificates", body) + provisioning_request_client.post("#{Version::V1}/certificates", body) end def delete_certificate(certificate_id: nil) raise "Certificate id is nil" if certificate_id.nil? - provisioning_request_client.delete("certificates/#{certificate_id}") + provisioning_request_client.delete("#{Version::V1}/certificates/#{certificate_id}") end # @@ -167,9 +176,9 @@ def delete_certificate(certificate_id: nil) def get_devices(profile_id: nil, filter: {}, includes: nil, fields: nil, limit: nil, sort: nil) params = provisioning_request_client.build_params(filter: filter, includes: includes, fields: fields, limit: limit, sort: sort) if profile_id.nil? - provisioning_request_client.get("devices", params) + provisioning_request_client.get("#{Version::V1}/devices", params) else - provisioning_request_client.get("profiles/#{profile_id}/devices", params) + provisioning_request_client.get("#{Version::V1}/profiles/#{profile_id}/devices", params) end end @@ -187,7 +196,7 @@ def post_device(name: nil, platform: nil, udid: nil) } } - provisioning_request_client.post("devices", body) + provisioning_request_client.post("#{Version::V1}/devices", body) end def patch_device(id: nil, status: nil, new_name: nil) @@ -206,7 +215,7 @@ def patch_device(id: nil, status: nil, new_name: nil) } } - provisioning_request_client.patch("devices/#{id}", body) + provisioning_request_client.patch("#{Version::V1}/devices/#{id}", body) end # @@ -215,7 +224,7 @@ def patch_device(id: nil, status: nil, new_name: nil) def get_profiles(filter: {}, includes: nil, fields: nil, limit: nil, sort: nil) params = provisioning_request_client.build_params(filter: filter, includes: includes, fields: fields, limit: limit, sort: sort) - provisioning_request_client.get("profiles", params) + provisioning_request_client.get("#{Version::V1}/profiles", params) end def post_profiles(bundle_id_id: nil, certificates: nil, devices: nil, attributes: {}) @@ -250,19 +259,19 @@ def post_profiles(bundle_id_id: nil, certificates: nil, devices: nil, attributes } } - provisioning_request_client.post("profiles", body) + provisioning_request_client.post("#{Version::V1}/profiles", body) end def get_profile_bundle_id(profile_id: nil) raise "Profile id is nil" if profile_id.nil? - provisioning_request_client.get("profiles/#{profile_id}/bundleId") + provisioning_request_client.get("#{Version::V1}/profiles/#{profile_id}/bundleId") end def delete_profile(profile_id: nil) raise "Profile id is nil" if profile_id.nil? - provisioning_request_client.delete("profiles/#{profile_id}") + provisioning_request_client.delete("#{Version::V1}/profiles/#{profile_id}") end end end diff --git a/spaceship/lib/spaceship/connect_api/testflight/client.rb b/spaceship/lib/spaceship/connect_api/testflight/client.rb index 7b307cc6811..8d63d672d53 100644 --- a/spaceship/lib/spaceship/connect_api/testflight/client.rb +++ b/spaceship/lib/spaceship/connect_api/testflight/client.rb @@ -19,7 +19,7 @@ def initialize(cookie: nil, current_team_id: nil, token: nil, another_client: ni end def self.hostname - 'https://appstoreconnect.apple.com/iris/v1/' + 'https://appstoreconnect.apple.com/iris/' end end end diff --git a/spaceship/lib/spaceship/connect_api/testflight/testflight.rb b/spaceship/lib/spaceship/connect_api/testflight/testflight.rb index 7651f968359..f66b81f43c1 100644 --- a/spaceship/lib/spaceship/connect_api/testflight/testflight.rb +++ b/spaceship/lib/spaceship/connect_api/testflight/testflight.rb @@ -4,6 +4,10 @@ module Spaceship class ConnectAPI module TestFlight module API + module Version + V1 = "v1" + end + def test_flight_request_client=(test_flight_request_client) @test_flight_request_client = test_flight_request_client end @@ -19,12 +23,12 @@ def test_flight_request_client def get_apps(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("apps", params) + test_flight_request_client.get("#{Version::V1}/apps", params) end def get_app(app_id: nil, includes: nil) params = test_flight_request_client.build_params(filter: nil, includes: includes, limit: nil, sort: nil) - test_flight_request_client.get("apps/#{app_id}", params) + test_flight_request_client.get("#{Version::V1}/apps/#{app_id}", params) end # @@ -33,7 +37,7 @@ def get_app(app_id: nil, includes: nil) def get_beta_app_localizations(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("betaAppLocalizations", params) + test_flight_request_client.get("#{Version::V1}/betaAppLocalizations", params) end def post_beta_app_localizations(app_id: nil, attributes: {}) @@ -52,7 +56,7 @@ def post_beta_app_localizations(app_id: nil, attributes: {}) } } - test_flight_request_client.post("betaAppLocalizations", body) + test_flight_request_client.post("#{Version::V1}/betaAppLocalizations", body) end def patch_beta_app_localizations(localization_id: nil, attributes: {}) @@ -64,7 +68,7 @@ def patch_beta_app_localizations(localization_id: nil, attributes: {}) } } - test_flight_request_client.patch("betaAppLocalizations/#{localization_id}", body) + test_flight_request_client.patch("#{Version::V1}/betaAppLocalizations/#{localization_id}", body) end # @@ -73,7 +77,7 @@ def patch_beta_app_localizations(localization_id: nil, attributes: {}) def get_beta_app_review_detail(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("betaAppReviewDetails", params) + test_flight_request_client.get("#{Version::V1}/betaAppReviewDetails", params) end def patch_beta_app_review_detail(app_id: nil, attributes: {}) @@ -85,7 +89,7 @@ def patch_beta_app_review_detail(app_id: nil, attributes: {}) } } - test_flight_request_client.patch("betaAppReviewDetails/#{app_id}", body) + test_flight_request_client.patch("#{Version::V1}/betaAppReviewDetails/#{app_id}", body) end # @@ -94,7 +98,7 @@ def patch_beta_app_review_detail(app_id: nil, attributes: {}) def get_beta_app_review_submissions(filter: {}, includes: nil, limit: nil, sort: nil, cursor: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort, cursor: cursor) - test_flight_request_client.get("betaAppReviewSubmissions", params) + test_flight_request_client.get("#{Version::V1}/betaAppReviewSubmissions", params) end def post_beta_app_review_submissions(build_id: nil) @@ -112,12 +116,12 @@ def post_beta_app_review_submissions(build_id: nil) } } - test_flight_request_client.post("betaAppReviewSubmissions", body) + test_flight_request_client.post("#{Version::V1}/betaAppReviewSubmissions", body) end def delete_beta_app_review_submission(beta_app_review_submission_id: nil) params = test_flight_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil, cursor: nil) - test_flight_request_client.delete("betaAppReviewSubmissions/#{beta_app_review_submission_id}", params) + test_flight_request_client.delete("#{Version::V1}/betaAppReviewSubmissions/#{beta_app_review_submission_id}", params) end # @@ -126,7 +130,7 @@ def delete_beta_app_review_submission(beta_app_review_submission_id: nil) def get_beta_build_localizations(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("betaBuildLocalizations", params) + test_flight_request_client.get("#{Version::V1}/betaBuildLocalizations", params) end def post_beta_build_localizations(build_id: nil, attributes: {}) @@ -145,7 +149,7 @@ def post_beta_build_localizations(build_id: nil, attributes: {}) } } - test_flight_request_client.post("betaBuildLocalizations", body) + test_flight_request_client.post("#{Version::V1}/betaBuildLocalizations", body) end def patch_beta_build_localizations(localization_id: nil, feedbackEmail: nil, attributes: {}) @@ -157,7 +161,7 @@ def patch_beta_build_localizations(localization_id: nil, feedbackEmail: nil, att } } - test_flight_request_client.patch("betaBuildLocalizations/#{localization_id}", body) + test_flight_request_client.patch("#{Version::V1}/betaBuildLocalizations/#{localization_id}", body) end # @@ -166,7 +170,7 @@ def patch_beta_build_localizations(localization_id: nil, feedbackEmail: nil, att def get_beta_build_metrics(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("betaBuildMetrics", params) + test_flight_request_client.get("#{Version::V1}/betaBuildMetrics", params) end # @@ -175,7 +179,7 @@ def get_beta_build_metrics(filter: {}, includes: nil, limit: nil, sort: nil) def get_beta_groups(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("betaGroups", params) + test_flight_request_client.get("#{Version::V1}/betaGroups", params) end def add_beta_groups_to_build(build_id: nil, beta_group_ids: []) @@ -188,7 +192,7 @@ def add_beta_groups_to_build(build_id: nil, beta_group_ids: []) end } - test_flight_request_client.post("builds/#{build_id}/relationships/betaGroups", body) + test_flight_request_client.post("#{Version::V1}/builds/#{build_id}/relationships/betaGroups", body) end def delete_beta_groups_from_build(build_id: nil, beta_group_ids: []) @@ -201,7 +205,7 @@ def delete_beta_groups_from_build(build_id: nil, beta_group_ids: []) end } - test_flight_request_client.delete("builds/#{build_id}/relationships/betaGroups", nil, body) + test_flight_request_client.delete("#{Version::V1}/builds/#{build_id}/relationships/betaGroups", nil, body) end def create_beta_group(app_id: nil, group_name: nil, is_internal_group: false, public_link_enabled: false, public_link_limit: 10_000, public_link_limit_enabled: false, has_access_to_all_builds: nil) @@ -232,7 +236,7 @@ def create_beta_group(app_id: nil, group_name: nil, is_internal_group: false, pu type: "betaGroups", }, } - test_flight_request_client.post("betaGroups", body) + test_flight_request_client.post("#{Version::V1}/betaGroups", body) end def patch_group(group_id: nil, attributes: {}) @@ -244,19 +248,19 @@ def patch_group(group_id: nil, attributes: {}) } } - test_flight_request_client.patch("betaGroups/#{group_id}", body) + test_flight_request_client.patch("#{Version::V1}/betaGroups/#{group_id}", body) end def delete_beta_group(group_id: nil) raise "group_id is nil" if group_id.nil? - test_flight_request_client.delete("betaGroups/#{group_id}") + test_flight_request_client.delete("#{Version::V1}/betaGroups/#{group_id}") end def get_builds_for_beta_group(group_id: nil) raise "group_id is nil" if group_id.nil? - test_flight_request_client.get("betaGroups/#{group_id}/builds") + test_flight_request_client.get("#{Version::V1}/betaGroups/#{group_id}/builds") end # @@ -265,7 +269,7 @@ def get_builds_for_beta_group(group_id: nil) def get_beta_testers(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("betaTesters", params) + test_flight_request_client.get("#{Version::V1}/betaTesters", params) end # beta_testers - [{email: "", firstName: "", lastName: ""}] @@ -293,7 +297,7 @@ def post_bulk_beta_tester_assignments(beta_group_id: nil, beta_testers: nil) } } - test_flight_request_client.post("bulkBetaTesterAssignments", body) + test_flight_request_client.post("#{Version::V1}/bulkBetaTesterAssignments", body) end # attributes - {email: "", firstName: "", lastName: ""} @@ -315,7 +319,7 @@ def post_beta_tester_assignment(beta_group_ids: [], attributes: {}) } } - test_flight_request_client.post("betaTesters", body) + test_flight_request_client.post("#{Version::V1}/betaTesters", body) end def add_beta_tester_to_group(beta_group_id: nil, beta_tester_ids: nil) @@ -328,7 +332,7 @@ def add_beta_tester_to_group(beta_group_id: nil, beta_tester_ids: nil) } end } - test_flight_request_client.post("betaGroups/#{beta_group_id}/relationships/betaTesters", body) + test_flight_request_client.post("#{Version::V1}/betaGroups/#{beta_group_id}/relationships/betaTesters", body) end def delete_beta_tester_from_apps(beta_tester_id: nil, app_ids: []) @@ -341,7 +345,7 @@ def delete_beta_tester_from_apps(beta_tester_id: nil, app_ids: []) end } - test_flight_request_client.delete("betaTesters/#{beta_tester_id}/relationships/apps", nil, body) + test_flight_request_client.delete("#{Version::V1}/betaTesters/#{beta_tester_id}/relationships/apps", nil, body) end def delete_beta_tester_from_beta_groups(beta_tester_id: nil, beta_group_ids: []) @@ -354,7 +358,7 @@ def delete_beta_tester_from_beta_groups(beta_tester_id: nil, beta_group_ids: []) end } - test_flight_request_client.delete("betaTesters/#{beta_tester_id}/relationships/betaGroups", nil, body) + test_flight_request_client.delete("#{Version::V1}/betaTesters/#{beta_tester_id}/relationships/betaGroups", nil, body) end def delete_beta_testers_from_app(beta_tester_ids: [], app_id: nil) @@ -367,7 +371,7 @@ def delete_beta_testers_from_app(beta_tester_ids: [], app_id: nil) end } - test_flight_request_client.delete("apps/#{app_id}/relationships/betaTesters", nil, body) + test_flight_request_client.delete("#{Version::V1}/apps/#{app_id}/relationships/betaTesters", nil, body) end def add_beta_tester_to_builds(beta_tester_id: nil, build_ids: []) @@ -380,7 +384,7 @@ def add_beta_tester_to_builds(beta_tester_id: nil, build_ids: []) end } - test_flight_request_client.post("betaTesters/#{beta_tester_id}/relationships/builds", body) + test_flight_request_client.post("#{Version::V1}/betaTesters/#{beta_tester_id}/relationships/builds", body) end def add_beta_testers_to_build(build_id: nil, beta_tester_ids: []) @@ -393,7 +397,7 @@ def add_beta_testers_to_build(build_id: nil, beta_tester_ids: []) end } - test_flight_request_client.post("builds/#{build_id}/relationships/individualTesters", body) + test_flight_request_client.post("#{Version::V1}/builds/#{build_id}/relationships/individualTesters", body) end def delete_beta_testers_from_build(build_id: nil, beta_tester_ids: []) @@ -406,7 +410,7 @@ def delete_beta_testers_from_build(build_id: nil, beta_tester_ids: []) end } - test_flight_request_client.delete("builds/#{build_id}/relationships/individualTesters", nil, body) + test_flight_request_client.delete("#{Version::V1}/builds/#{build_id}/relationships/individualTesters", nil, body) end # @@ -415,7 +419,7 @@ def delete_beta_testers_from_build(build_id: nil, beta_tester_ids: []) def get_beta_tester_metrics(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("betaTesterMetrics", params) + test_flight_request_client.get("#{Version::V1}/betaTesterMetrics", params) end # @@ -424,7 +428,7 @@ def get_beta_tester_metrics(filter: {}, includes: nil, limit: nil, sort: nil) def get_build_bundles_build_bundle_file_sizes(build_bundle_id:, limit: nil) params = test_flight_request_client.build_params(filter: nil, includes: nil, limit: limit, sort: nil, cursor: nil) - test_flight_request_client.get("buildBundles/#{build_bundle_id}/buildBundleFileSizes", params) + test_flight_request_client.get("#{Version::V1}/buildBundles/#{build_bundle_id}/buildBundleFileSizes", params) end # @@ -433,16 +437,16 @@ def get_build_bundles_build_bundle_file_sizes(build_bundle_id:, limit: nil) def get_builds(filter: {}, includes: "buildBetaDetail,betaBuildMetrics", limit: 10, sort: "uploadedDate", cursor: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort, cursor: cursor) - test_flight_request_client.get("builds", params) + test_flight_request_client.get("#{Version::V1}/builds", params) end def get_build(build_id: nil, app_store_version_id: nil, includes: nil) if build_id params = test_flight_request_client.build_params(filter: nil, includes: includes, limit: nil, sort: nil, cursor: nil) - return test_flight_request_client.get("builds/#{build_id}", params) + return test_flight_request_client.get("#{Version::V1}/builds/#{build_id}", params) elsif app_store_version_id params = test_flight_request_client.build_params(filter: nil, includes: includes, limit: nil, sort: nil, cursor: nil) - return test_flight_request_client.get("appStoreVersions/#{app_store_version_id}/build", params) + return test_flight_request_client.get("#{Version::V1}/appStoreVersions/#{app_store_version_id}/build", params) else return nil end @@ -457,7 +461,7 @@ def patch_builds(build_id: nil, attributes: {}) } } - test_flight_request_client.patch("builds/#{build_id}", body) + test_flight_request_client.patch("#{Version::V1}/builds/#{build_id}", body) end # @@ -466,7 +470,7 @@ def patch_builds(build_id: nil, attributes: {}) def get_build_beta_details(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("buildBetaDetails", params) + test_flight_request_client.get("#{Version::V1}/buildBetaDetails", params) end def patch_build_beta_details(build_beta_details_id: nil, attributes: {}) @@ -478,7 +482,7 @@ def patch_build_beta_details(build_beta_details_id: nil, attributes: {}) } } - test_flight_request_client.patch("buildBetaDetails/#{build_beta_details_id}", body) + test_flight_request_client.patch("#{Version::V1}/buildBetaDetails/#{build_beta_details_id}", body) end # @@ -487,7 +491,7 @@ def patch_build_beta_details(build_beta_details_id: nil, attributes: {}) def get_build_deliveries(app_id:, filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("apps/#{app_id}/buildDeliveries", params) + test_flight_request_client.get("#{Version::V1}/apps/#{app_id}/buildDeliveries", params) end # @@ -496,7 +500,7 @@ def get_build_deliveries(app_id:, filter: {}, includes: nil, limit: nil, sort: n def get_pre_release_versions(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("preReleaseVersions", params) + test_flight_request_client.get("#{Version::V1}/preReleaseVersions", params) end # @@ -505,13 +509,13 @@ def get_pre_release_versions(filter: {}, includes: nil, limit: nil, sort: nil) def get_beta_feedback(filter: {}, includes: nil, limit: nil, sort: nil) params = test_flight_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - test_flight_request_client.get("betaFeedbacks", params) + test_flight_request_client.get("#{Version::V1}/betaFeedbacks", params) end def delete_beta_feedback(feedback_id: nil) raise "Feedback id is nil" if feedback_id.nil? - test_flight_request_client.delete("betaFeedbacks/#{feedback_id}") + test_flight_request_client.delete("#{Version::V1}/betaFeedbacks/#{feedback_id}") end end end diff --git a/spaceship/lib/spaceship/connect_api/token.rb b/spaceship/lib/spaceship/connect_api/token.rb index ce3e922697d..b67f677be3b 100644 --- a/spaceship/lib/spaceship/connect_api/token.rb +++ b/spaceship/lib/spaceship/connect_api/token.rb @@ -39,7 +39,6 @@ def self.from_json_file(filepath) missing_keys = [] missing_keys << 'key_id' unless json.key?(:key_id) - missing_keys << 'issuer_id' unless json.key?(:issuer_id) missing_keys << 'key' unless json.key?(:key) unless missing_keys.empty? @@ -99,11 +98,18 @@ def refresh! } payload = { - iss: issuer_id, - iat: now.to_i, + # Reduce the issued-at-time in case our time is slighly ahead of Apple's servers, which causes the token to be rejected. + iat: now.to_i - 60, exp: @expiration.to_i, aud: 'appstoreconnect-v1' } + if issuer_id + payload[:iss] = issuer_id + else + # Consider the key as individual key. + # https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests#4313913 + payload[:sub] = 'user' + end @text = JWT.encode(payload, @key, 'ES256', header) end diff --git a/spaceship/lib/spaceship/connect_api/tunes/client.rb b/spaceship/lib/spaceship/connect_api/tunes/client.rb index a1bb7764f34..c711d127b3b 100644 --- a/spaceship/lib/spaceship/connect_api/tunes/client.rb +++ b/spaceship/lib/spaceship/connect_api/tunes/client.rb @@ -19,7 +19,7 @@ def initialize(cookie: nil, current_team_id: nil, token: nil, another_client: ni end def self.hostname - 'https://appstoreconnect.apple.com/iris/v1/' + 'https://appstoreconnect.apple.com/iris/' end end end diff --git a/spaceship/lib/spaceship/connect_api/tunes/tunes.rb b/spaceship/lib/spaceship/connect_api/tunes/tunes.rb index e2bf9721e89..1db6c64c67c 100644 --- a/spaceship/lib/spaceship/connect_api/tunes/tunes.rb +++ b/spaceship/lib/spaceship/connect_api/tunes/tunes.rb @@ -4,6 +4,12 @@ module Spaceship class ConnectAPI module Tunes module API + module Version + V1 = "v1" + V2 = "v2" + V3 = "v3" + end + def tunes_request_client=(tunes_request_client) @tunes_request_client = tunes_request_client end @@ -21,7 +27,7 @@ def get_age_rating_declaration(app_info_id: nil, app_store_version_id: nil) raise "Keyword 'app_store_version_id' is deprecated and 'app_info_id' is required" if app_store_version_id || app_info_id.nil? params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.get("appInfos/#{app_info_id}/ageRatingDeclaration", params) + tunes_request_client.get("#{Version::V1}/appInfos/#{app_info_id}/ageRatingDeclaration", params) end def patch_age_rating_declaration(age_rating_declaration_id: nil, attributes: nil) @@ -33,7 +39,7 @@ def patch_age_rating_declaration(age_rating_declaration_id: nil, attributes: nil } } - tunes_request_client.patch("ageRatingDeclarations/#{age_rating_declaration_id}", body) + tunes_request_client.patch("#{Version::V1}/ageRatingDeclarations/#{age_rating_declaration_id}", body) end # @@ -131,7 +137,7 @@ def post_app(name: nil, version_string: nil, sku: nil, primary_locale: nil, bund included: included } - tunes_request_client.post("apps", body) + tunes_request_client.post("#{Version::V1}/apps", body) end # Updates app attributes, price tier, visibility in regions or countries. @@ -206,7 +212,7 @@ def patch_app(app_id: nil, attributes: {}, app_price_tier_id: nil, territory_ids } body[:included] = included unless included.empty? - tunes_request_client.patch("apps/#{app_id}", body) + tunes_request_client.patch("#{Version::V1}/apps/#{app_id}", body) end # @@ -215,7 +221,7 @@ def patch_app(app_id: nil, attributes: {}, app_price_tier_id: nil, territory_ids def get_app_data_usages(app_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("apps/#{app_id}/dataUsages", params) + tunes_request_client.get("#{Version::V1}/apps/#{app_id}/dataUsages", params) end def post_app_data_usage(app_id:, app_data_usage_category_id: nil, app_data_usage_protection_id: nil, app_data_usage_purpose_id: nil) @@ -264,11 +270,11 @@ def post_app_data_usage(app_id:, app_data_usage_category_id: nil, app_data_usage } } - tunes_request_client.post("appDataUsages", body) + tunes_request_client.post("#{Version::V1}/appDataUsages", body) end def delete_app_data_usage(app_data_usage_id: nil) - tunes_request_client.delete("appDataUsages/#{app_data_usage_id}") + tunes_request_client.delete("#{Version::V1}/appDataUsages/#{app_data_usage_id}") end # @@ -277,7 +283,7 @@ def delete_app_data_usage(app_data_usage_id: nil) def get_app_data_usage_categories(filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appDataUsageCategories", params) + tunes_request_client.get("#{Version::V1}/appDataUsageCategories", params) end # @@ -286,7 +292,7 @@ def get_app_data_usage_categories(filter: {}, includes: nil, limit: nil, sort: n def get_app_data_usage_purposes(filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appDataUsagePurposes", params) + tunes_request_client.get("#{Version::V1}/appDataUsagePurposes", params) end # @@ -295,7 +301,7 @@ def get_app_data_usage_purposes(filter: {}, includes: nil, limit: nil, sort: nil def get_app_data_usages_publish_state(app_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.get("apps/#{app_id}/dataUsagePublishState", params) + tunes_request_client.get("#{Version::V1}/apps/#{app_id}/dataUsagePublishState", params) end def patch_app_data_usages_publish_state(app_data_usages_publish_state_id: nil, published: nil) @@ -309,7 +315,7 @@ def patch_app_data_usages_publish_state(app_data_usages_publish_state_id: nil, p } } - tunes_request_client.patch("appDataUsagesPublishState/#{app_data_usages_publish_state_id}", body) + tunes_request_client.patch("#{Version::V1}/appDataUsagesPublishState/#{app_data_usages_publish_state_id}", body) end # @@ -318,7 +324,7 @@ def patch_app_data_usages_publish_state(app_data_usages_publish_state_id: nil, p def get_app_preview(app_preview_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.get("appPreviews/#{app_preview_id}", params) + tunes_request_client.get("#{Version::V1}/appPreviews/#{app_preview_id}", params) end def post_app_preview(app_preview_set_id: nil, attributes: {}) @@ -337,7 +343,7 @@ def post_app_preview(app_preview_set_id: nil, attributes: {}) } } - tunes_request_client.post("appPreviews", body) + tunes_request_client.post("#{Version::V1}/appPreviews", body) end def patch_app_preview(app_preview_id: nil, attributes: {}) @@ -349,12 +355,12 @@ def patch_app_preview(app_preview_id: nil, attributes: {}) } } - tunes_request_client.patch("appPreviews/#{app_preview_id}", body) + tunes_request_client.patch("#{Version::V1}/appPreviews/#{app_preview_id}", body) end def delete_app_preview(app_preview_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("appPreviews/#{app_preview_id}", params) + tunes_request_client.delete("#{Version::V1}/appPreviews/#{app_preview_id}", params) end # @@ -363,12 +369,12 @@ def delete_app_preview(app_preview_id: nil) def get_app_preview_sets(filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appPreviewSets", params) + tunes_request_client.get("#{Version::V1}/appPreviewSets", params) end def get_app_preview_set(app_preview_set_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appPreviewSets/#{app_preview_set_id}", params) + tunes_request_client.get("#{Version::V1}/appPreviewSets/#{app_preview_set_id}", params) end def post_app_preview_set(app_store_version_localization_id: nil, attributes: {}) @@ -387,12 +393,12 @@ def post_app_preview_set(app_store_version_localization_id: nil, attributes: {}) } } - tunes_request_client.post("appPreviewSets", body) + tunes_request_client.post("#{Version::V1}/appPreviewSets", body) end def delete_app_preview_set(app_preview_set_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("appPreviewSets/#{app_preview_set_id}", params) + tunes_request_client.delete("#{Version::V1}/appPreviewSets/#{app_preview_set_id}", params) end def patch_app_preview_set_previews(app_preview_set_id: nil, app_preview_ids: nil) @@ -407,7 +413,16 @@ def patch_app_preview_set_previews(app_preview_set_id: nil, app_preview_ids: nil end } - tunes_request_client.patch("appPreviewSets/#{app_preview_set_id}/relationships/appPreviews", body) + tunes_request_client.patch("#{Version::V1}/appPreviewSets/#{app_preview_set_id}/relationships/appPreviews", body) + end + + # + # appAvailabilities + # + + def get_app_availabilities(app_id: nil, filter: nil, includes: nil, limit: nil, sort: nil) + params = tunes_request_client.build_params(filter: nil, includes: includes, limit: limit, sort: nil) + tunes_request_client.get("#{Version::V2}/appAvailabilities/#{app_id}", params) end # @@ -416,7 +431,7 @@ def patch_app_preview_set_previews(app_preview_set_id: nil, app_preview_ids: nil def get_available_territories(app_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("apps/#{app_id}/availableTerritories", params) + tunes_request_client.get("#{Version::V1}/apps/#{app_id}/availableTerritories", params) end # @@ -425,12 +440,12 @@ def get_available_territories(app_id: nil, filter: {}, includes: nil, limit: nil def get_app_prices(app_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appPrices", params) + tunes_request_client.get("#{Version::V1}/appPrices", params) end def get_app_price(app_price_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appPrices/#{app_price_id}", params) + tunes_request_client.get("#{Version::V1}/appPrices/#{app_price_id}", params) end # @@ -438,7 +453,7 @@ def get_app_price(app_price_id: nil, filter: {}, includes: nil, limit: nil, sort # def get_app_price_points(filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appPricePoints", params) + tunes_request_client.get("#{Version::V1}/appPricePoints", params) end # @@ -461,7 +476,7 @@ def post_app_store_review_attachment(app_store_review_detail_id: nil, attributes } } - tunes_request_client.post("appStoreReviewAttachments", body) + tunes_request_client.post("#{Version::V1}/appStoreReviewAttachments", body) end def patch_app_store_review_attachment(app_store_review_attachment_id: nil, attributes: {}) @@ -473,12 +488,12 @@ def patch_app_store_review_attachment(app_store_review_attachment_id: nil, attri } } - tunes_request_client.patch("appStoreReviewAttachments/#{app_store_review_attachment_id}", body) + tunes_request_client.patch("#{Version::V1}/appStoreReviewAttachments/#{app_store_review_attachment_id}", body) end def delete_app_store_review_attachment(app_store_review_attachment_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("appStoreReviewAttachments/#{app_store_review_attachment_id}", params) + tunes_request_client.delete("#{Version::V1}/appStoreReviewAttachments/#{app_store_review_attachment_id}", params) end # @@ -487,12 +502,12 @@ def delete_app_store_review_attachment(app_store_review_attachment_id: nil) def get_app_screenshot_sets(app_store_version_localization_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appStoreVersionLocalizations/#{app_store_version_localization_id}/appScreenshotSets", params) + tunes_request_client.get("#{Version::V1}/appStoreVersionLocalizations/#{app_store_version_localization_id}/appScreenshotSets", params) end def get_app_screenshot_set(app_screenshot_set_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appScreenshotSets/#{app_screenshot_set_id}", params) + tunes_request_client.get("#{Version::V1}/appScreenshotSets/#{app_screenshot_set_id}", params) end def post_app_screenshot_set(app_store_version_localization_id: nil, attributes: {}) @@ -511,7 +526,7 @@ def post_app_screenshot_set(app_store_version_localization_id: nil, attributes: } } - tunes_request_client.post("appScreenshotSets", body) + tunes_request_client.post("#{Version::V1}/appScreenshotSets", body) end def patch_app_screenshot_set_screenshots(app_screenshot_set_id: nil, app_screenshot_ids: nil) @@ -526,12 +541,12 @@ def patch_app_screenshot_set_screenshots(app_screenshot_set_id: nil, app_screens end } - tunes_request_client.patch("appScreenshotSets/#{app_screenshot_set_id}/relationships/appScreenshots", body) + tunes_request_client.patch("#{Version::V1}/appScreenshotSets/#{app_screenshot_set_id}/relationships/appScreenshots", body) end def delete_app_screenshot_set(app_screenshot_set_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("appScreenshotSets/#{app_screenshot_set_id}", params) + tunes_request_client.delete("#{Version::V1}/appScreenshotSets/#{app_screenshot_set_id}", params) end # @@ -540,7 +555,7 @@ def delete_app_screenshot_set(app_screenshot_set_id: nil) def get_app_screenshot(app_screenshot_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.get("appScreenshots/#{app_screenshot_id}", params) + tunes_request_client.get("#{Version::V1}/appScreenshots/#{app_screenshot_id}", params) end def post_app_screenshot(app_screenshot_set_id: nil, attributes: {}) @@ -559,7 +574,7 @@ def post_app_screenshot(app_screenshot_set_id: nil, attributes: {}) } } - tunes_request_client.post("appScreenshots", body, tries: 1) + tunes_request_client.post("#{Version::V1}/appScreenshots", body, tries: 1) end def patch_app_screenshot(app_screenshot_id: nil, attributes: {}) @@ -571,12 +586,12 @@ def patch_app_screenshot(app_screenshot_id: nil, attributes: {}) } } - tunes_request_client.patch("appScreenshots/#{app_screenshot_id}", body) + tunes_request_client.patch("#{Version::V1}/appScreenshots/#{app_screenshot_id}", body) end def delete_app_screenshot(app_screenshot_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("appScreenshots/#{app_screenshot_id}", params) + tunes_request_client.delete("#{Version::V1}/appScreenshots/#{app_screenshot_id}", params) end # @@ -585,7 +600,7 @@ def delete_app_screenshot(app_screenshot_id: nil) def get_app_infos(app_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("apps/#{app_id}/appInfos", params) + tunes_request_client.get("#{Version::V1}/apps/#{app_id}/appInfos", params) end def patch_app_info(app_info_id: nil, attributes: {}) @@ -601,7 +616,7 @@ def patch_app_info(app_info_id: nil, attributes: {}) data: data } - tunes_request_client.patch("appInfos/#{app_info_id}", body) + tunes_request_client.patch("#{Version::V1}/appInfos/#{app_info_id}", body) end # @@ -679,12 +694,12 @@ def patch_app_info_categories(app_info_id: nil, category_id_map: nil) data: data } - tunes_request_client.patch("appInfos/#{app_info_id}", body) + tunes_request_client.patch("#{Version::V1}/appInfos/#{app_info_id}", body) end def delete_app_info(app_info_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("appInfos/#{app_info_id}", params) + tunes_request_client.delete("#{Version::V1}/appInfos/#{app_info_id}", params) end # @@ -693,7 +708,7 @@ def delete_app_info(app_info_id: nil) def get_app_info_localizations(app_info_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appInfos/#{app_info_id}/appInfoLocalizations", params) + tunes_request_client.get("#{Version::V1}/appInfos/#{app_info_id}/appInfoLocalizations", params) end def post_app_info_localization(app_info_id: nil, attributes: {}) @@ -712,7 +727,7 @@ def post_app_info_localization(app_info_id: nil, attributes: {}) } } - tunes_request_client.post("appInfoLocalizations", body) + tunes_request_client.post("#{Version::V1}/appInfoLocalizations", body) end def patch_app_info_localization(app_info_localization_id: nil, attributes: {}) @@ -724,7 +739,7 @@ def patch_app_info_localization(app_info_localization_id: nil, attributes: {}) } } - tunes_request_client.patch("appInfoLocalizations/#{app_info_localization_id}", body) + tunes_request_client.patch("#{Version::V1}/appInfoLocalizations/#{app_info_localization_id}", body) end # @@ -733,7 +748,7 @@ def patch_app_info_localization(app_info_localization_id: nil, attributes: {}) def get_app_store_review_detail(app_store_version_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appStoreVersions/#{app_store_version_id}/appStoreReviewDetail", params) + tunes_request_client.get("#{Version::V1}/appStoreVersions/#{app_store_version_id}/appStoreReviewDetail", params) end def post_app_store_review_detail(app_store_version_id: nil, attributes: {}) @@ -752,7 +767,7 @@ def post_app_store_review_detail(app_store_version_id: nil, attributes: {}) } } - tunes_request_client.post("appStoreReviewDetails", body) + tunes_request_client.post("#{Version::V1}/appStoreReviewDetails", body) end def patch_app_store_review_detail(app_store_review_detail_id: nil, attributes: {}) @@ -764,7 +779,7 @@ def patch_app_store_review_detail(app_store_review_detail_id: nil, attributes: { } } - tunes_request_client.patch("appStoreReviewDetails/#{app_store_review_detail_id}", body) + tunes_request_client.patch("#{Version::V1}/appStoreReviewDetails/#{app_store_review_detail_id}", body) end # @@ -773,12 +788,12 @@ def patch_app_store_review_detail(app_store_review_detail_id: nil, attributes: { def get_app_store_version_localizations(app_store_version_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("appStoreVersions/#{app_store_version_id}/appStoreVersionLocalizations", params) + tunes_request_client.get("#{Version::V1}/appStoreVersions/#{app_store_version_id}/appStoreVersionLocalizations", params) end def get_app_store_version_localization(app_store_version_localization_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.get("appStoreVersionLocalizations/#{app_store_version_localization_id}", params) + tunes_request_client.get("#{Version::V1}/appStoreVersionLocalizations/#{app_store_version_localization_id}", params) end def post_app_store_version_localization(app_store_version_id: nil, attributes: {}) @@ -797,7 +812,7 @@ def post_app_store_version_localization(app_store_version_id: nil, attributes: { } } - tunes_request_client.post("appStoreVersionLocalizations", body) + tunes_request_client.post("#{Version::V1}/appStoreVersionLocalizations", body) end def patch_app_store_version_localization(app_store_version_localization_id: nil, attributes: {}) @@ -809,12 +824,12 @@ def patch_app_store_version_localization(app_store_version_localization_id: nil, } } - tunes_request_client.patch("appStoreVersionLocalizations/#{app_store_version_localization_id}", body) + tunes_request_client.patch("#{Version::V1}/appStoreVersionLocalizations/#{app_store_version_localization_id}", body) end def delete_app_store_version_localization(app_store_version_localization_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("appStoreVersionLocalizations/#{app_store_version_localization_id}", params) + tunes_request_client.delete("#{Version::V1}/appStoreVersionLocalizations/#{app_store_version_localization_id}", params) end # @@ -823,7 +838,7 @@ def delete_app_store_version_localization(app_store_version_localization_id: nil def get_app_store_version_phased_release(app_store_version_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.get("appStoreVersions/#{app_store_version_id}/appStoreVersionPhasedRelease", params) + tunes_request_client.get("#{Version::V1}/appStoreVersions/#{app_store_version_id}/appStoreVersionPhasedRelease", params) end def post_app_store_version_phased_release(app_store_version_id: nil, attributes: {}) @@ -842,7 +857,7 @@ def post_app_store_version_phased_release(app_store_version_id: nil, attributes: } } - tunes_request_client.post("appStoreVersionPhasedReleases", body) + tunes_request_client.post("#{Version::V1}/appStoreVersionPhasedReleases", body) end def patch_app_store_version_phased_release(app_store_version_phased_release_id: nil, attributes: {}) @@ -854,12 +869,12 @@ def patch_app_store_version_phased_release(app_store_version_phased_release_id: } } - tunes_request_client.patch("appStoreVersionPhasedReleases/#{app_store_version_phased_release_id}", body) + tunes_request_client.patch("#{Version::V1}/appStoreVersionPhasedReleases/#{app_store_version_phased_release_id}", body) end def delete_app_store_version_phased_release(app_store_version_phased_release_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("appStoreVersionPhasedReleases/#{app_store_version_phased_release_id}", params) + tunes_request_client.delete("#{Version::V1}/appStoreVersionPhasedReleases/#{app_store_version_phased_release_id}", params) end # @@ -868,12 +883,12 @@ def delete_app_store_version_phased_release(app_store_version_phased_release_id: def get_app_store_versions(app_id: nil, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("apps/#{app_id}/appStoreVersions", params) + tunes_request_client.get("#{Version::V1}/apps/#{app_id}/appStoreVersions", params) end def get_app_store_version(app_store_version_id: nil, includes: nil) params = tunes_request_client.build_params(filter: nil, includes: includes, limit: nil, sort: nil) - tunes_request_client.get("appStoreVersions/#{app_store_version_id}", params) + tunes_request_client.get("#{Version::V1}/appStoreVersions/#{app_store_version_id}", params) end def post_app_store_version(app_id: nil, attributes: {}) @@ -892,7 +907,7 @@ def post_app_store_version(app_id: nil, attributes: {}) } } - tunes_request_client.post("appStoreVersions", body) + tunes_request_client.post("#{Version::V1}/appStoreVersions", body) end def patch_app_store_version(app_store_version_id: nil, attributes: {}) @@ -904,7 +919,7 @@ def patch_app_store_version(app_store_version_id: nil, attributes: {}) } } - tunes_request_client.patch("appStoreVersions/#{app_store_version_id}", body) + tunes_request_client.patch("#{Version::V1}/appStoreVersions/#{app_store_version_id}", body) end def patch_app_store_version_with_build(app_store_version_id: nil, build_id: nil) @@ -928,7 +943,7 @@ def patch_app_store_version_with_build(app_store_version_id: nil, build_id: nil) } } - tunes_request_client.patch("appStoreVersions/#{app_store_version_id}", body) + tunes_request_client.patch("#{Version::V1}/appStoreVersions/#{app_store_version_id}", body) end # @@ -937,7 +952,7 @@ def patch_app_store_version_with_build(app_store_version_id: nil, build_id: nil) def get_reset_ratings_request(app_store_version_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.get("appStoreVersions/#{app_store_version_id}/resetRatingsRequest", params) + tunes_request_client.get("#{Version::V1}/appStoreVersions/#{app_store_version_id}/resetRatingsRequest", params) end def post_reset_ratings_request(app_store_version_id: nil) @@ -955,12 +970,12 @@ def post_reset_ratings_request(app_store_version_id: nil) } } - tunes_request_client.post("resetRatingsRequests", body) + tunes_request_client.post("#{Version::V1}/resetRatingsRequests", body) end def delete_reset_ratings_request(reset_ratings_request_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("resetRatingsRequests/#{reset_ratings_request_id}", params) + tunes_request_client.delete("#{Version::V1}/resetRatingsRequests/#{reset_ratings_request_id}", params) end # @@ -969,7 +984,7 @@ def delete_reset_ratings_request(reset_ratings_request_id: nil) def get_app_store_version_submission(app_store_version_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.get("appStoreVersions/#{app_store_version_id}/appStoreVersionSubmission", params) + tunes_request_client.get("#{Version::V1}/appStoreVersions/#{app_store_version_id}/appStoreVersionSubmission", params) end def post_app_store_version_submission(app_store_version_id: nil) @@ -987,12 +1002,12 @@ def post_app_store_version_submission(app_store_version_id: nil) } } - tunes_request_client.post("appStoreVersionSubmissions", body) + tunes_request_client.post("#{Version::V1}/appStoreVersionSubmissions", body) end def delete_app_store_version_submission(app_store_version_submission_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("appStoreVersionSubmissions/#{app_store_version_submission_id}", params) + tunes_request_client.delete("#{Version::V1}/appStoreVersionSubmissions/#{app_store_version_submission_id}", params) end # @@ -1014,7 +1029,7 @@ def post_app_store_version_release_request(app_store_version_id: nil) } } - tunes_request_client.post("appStoreVersionReleaseRequests", body) + tunes_request_client.post("#{Version::V1}/appStoreVersionReleaseRequests", body) end # @@ -1023,7 +1038,7 @@ def post_app_store_version_release_request(app_store_version_id: nil) def get_custom_app_users(app_id: nil, filter: nil, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("apps/#{app_id}/customAppUsers", params) + tunes_request_client.get("#{Version::V1}/apps/#{app_id}/customAppUsers", params) end def post_custom_app_user(app_id: nil, apple_id: nil) @@ -1044,12 +1059,12 @@ def post_custom_app_user(app_id: nil, apple_id: nil) } } - tunes_request_client.post("customAppUsers", body) + tunes_request_client.post("#{Version::V1}/customAppUsers", body) end def delete_custom_app_user(custom_app_user_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("customAppUsers/#{custom_app_user_id}", params) + tunes_request_client.delete("#{Version::V1}/customAppUsers/#{custom_app_user_id}", params) end # @@ -1058,7 +1073,7 @@ def delete_custom_app_user(custom_app_user_id: nil) def get_custom_app_organization(app_id: nil, filter: nil, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("apps/#{app_id}/customAppOrganizations", params) + tunes_request_client.get("#{Version::V1}/apps/#{app_id}/customAppOrganizations", params) end def post_custom_app_organization(app_id: nil, device_enrollment_program_id: nil, name: nil) @@ -1080,57 +1095,12 @@ def post_custom_app_organization(app_id: nil, device_enrollment_program_id: nil, } } - tunes_request_client.post("customAppOrganizations", body) + tunes_request_client.post("#{Version::V1}/customAppOrganizations", body) end def delete_custom_app_organization(custom_app_organization_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("customAppOrganizations/#{custom_app_organization_id}", params) - end - - # - # idfaDeclarations - # - - def get_idfa_declaration(app_store_version_id: nil) - params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.get("appStoreVersions/#{app_store_version_id}/idfaDeclaration", params) - end - - def post_idfa_declaration(app_store_version_id: nil, attributes: nil) - body = { - data: { - type: "idfaDeclarations", - attributes: attributes, - relationships: { - appStoreVersion: { - data: { - type: "appStoreVersions", - id: app_store_version_id - } - } - } - } - } - - tunes_request_client.post("idfaDeclarations", body) - end - - def patch_idfa_declaration(idfa_declaration_id: nil, attributes: nil) - body = { - data: { - type: "idfaDeclarations", - id: idfa_declaration_id, - attributes: attributes - } - } - - tunes_request_client.patch("idfaDeclarations/#{idfa_declaration_id}", body) - end - - def delete_idfa_declaration(idfa_declaration_id: nil) - params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("idfaDeclarations/#{idfa_declaration_id}", params) + tunes_request_client.delete("#{Version::V1}/customAppOrganizations/#{custom_app_organization_id}", params) end # @@ -1139,12 +1109,12 @@ def delete_idfa_declaration(idfa_declaration_id: nil) def get_review_submissions(app_id:, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("apps/#{app_id}/reviewSubmissions", params) + tunes_request_client.get("#{Version::V1}/apps/#{app_id}/reviewSubmissions", params) end def get_review_submission(review_submission_id:, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("reviewSubmissions/#{review_submission_id}", params) + tunes_request_client.get("#{Version::V1}/reviewSubmissions/#{review_submission_id}", params) end def post_review_submission(app_id:, platform:) @@ -1165,7 +1135,7 @@ def post_review_submission(app_id:, platform:) } } - tunes_request_client.post("reviewSubmissions", body) + tunes_request_client.post("#{Version::V1}/reviewSubmissions", body) end def patch_review_submission(review_submission_id:, attributes: nil) @@ -1177,7 +1147,7 @@ def patch_review_submission(review_submission_id:, attributes: nil) } } - tunes_request_client.patch("reviewSubmissions/#{review_submission_id}", body) + tunes_request_client.patch("#{Version::V1}/reviewSubmissions/#{review_submission_id}", body) end # @@ -1186,7 +1156,7 @@ def patch_review_submission(review_submission_id:, attributes: nil) def get_review_submission_items(review_submission_id:, filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("reviewSubmissions/#{review_submission_id}/items", params) + tunes_request_client.get("#{Version::V1}/reviewSubmissions/#{review_submission_id}/items", params) end def post_review_submission_item(review_submission_id:, app_store_version_id: nil) @@ -1213,7 +1183,7 @@ def post_review_submission_item(review_submission_id:, app_store_version_id: nil } end - tunes_request_client.post("reviewSubmissionItems", body) + tunes_request_client.post("#{Version::V1}/reviewSubmissionItems", body) end # @@ -1222,7 +1192,7 @@ def post_review_submission_item(review_submission_id:, app_store_version_id: nil def get_sandbox_testers(filter: nil, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - tunes_request_client.get("sandboxTesters", params) + tunes_request_client.get("#{Version::V1}/sandboxTesters", params) end def post_sandbox_tester(attributes: {}) @@ -1233,12 +1203,12 @@ def post_sandbox_tester(attributes: {}) } } - tunes_request_client.post("sandboxTesters", body) + tunes_request_client.post("#{Version::V1}/sandboxTesters", body) end def delete_sandbox_tester(sandbox_tester_id: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.delete("sandboxTesters/#{sandbox_tester_id}", params) + tunes_request_client.delete("#{Version::V1}/sandboxTesters/#{sandbox_tester_id}", params) end # @@ -1247,7 +1217,7 @@ def delete_sandbox_tester(sandbox_tester_id: nil) def get_territories(filter: {}, includes: nil, limit: nil, sort: nil) params = tunes_request_client.build_params(filter: nil, includes: nil, limit: nil, sort: nil) - tunes_request_client.get("territories", params) + tunes_request_client.get("#{Version::V1}/territories", params) end # @@ -1260,17 +1230,17 @@ def get_territories(filter: {}, includes: nil, limit: nil, sort: nil) def get_resolution_center_threads(filter: {}, includes: nil) params = tunes_request_client.build_params(filter: filter, includes: includes) - tunes_request_client.get('resolutionCenterThreads', params) + tunes_request_client.get("#{Version::V1}/resolutionCenterThreads", params) end def get_resolution_center_messages(thread_id:, filter: {}, includes: nil) params = tunes_request_client.build_params(filter: filter, includes: includes) - tunes_request_client.get("resolutionCenterThreads/#{thread_id}/resolutionCenterMessages", params) + tunes_request_client.get("#{Version::V1}/resolutionCenterThreads/#{thread_id}/resolutionCenterMessages", params) end def get_review_rejection(filter: {}, includes: nil) params = tunes_request_client.build_params(filter: filter, includes: includes) - tunes_request_client.get("reviewRejections", params) + tunes_request_client.get("#{Version::V1}/reviewRejections", params) end end end diff --git a/spaceship/lib/spaceship/connect_api/users/client.rb b/spaceship/lib/spaceship/connect_api/users/client.rb index 0270192f0d4..d8e99a2f2fb 100644 --- a/spaceship/lib/spaceship/connect_api/users/client.rb +++ b/spaceship/lib/spaceship/connect_api/users/client.rb @@ -19,7 +19,7 @@ def initialize(cookie: nil, current_team_id: nil, token: nil, another_client: ni end def self.hostname - 'https://appstoreconnect.apple.com/iris/v1/' + 'https://appstoreconnect.apple.com/iris/' end end end diff --git a/spaceship/lib/spaceship/connect_api/users/users.rb b/spaceship/lib/spaceship/connect_api/users/users.rb index c4c22b2324a..b4944522e91 100644 --- a/spaceship/lib/spaceship/connect_api/users/users.rb +++ b/spaceship/lib/spaceship/connect_api/users/users.rb @@ -4,6 +4,10 @@ module Spaceship class ConnectAPI module Users module API + module Version + V1 = "v1" + end + def users_request_client=(users_request_client) @users_request_client = users_request_client end @@ -20,12 +24,12 @@ def users_request_client # Get list of users def get_users(filter: {}, includes: nil, limit: nil, sort: nil) params = users_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - users_request_client.get("users", params) + users_request_client.get("#{Version::V1}/users", params) end # Delete existing user def delete_user(user_id: nil) - users_request_client.delete("users/#{user_id}") + users_request_client.delete("#{Version::V1}/users/#{user_id}") end # Update existing user @@ -55,7 +59,7 @@ def patch_user(user_id:, all_apps_visible:, provisioning_allowed:, roles:, visib # Avoid API error: You cannot set visible apps for this user because the user's roles give them access to all apps. body[:data].delete(:relationships) if all_apps_visible - users_request_client.patch("users/#{user_id}", body) + users_request_client.patch("#{Version::V1}/users/#{user_id}", body) end # Add app permissions for user @@ -74,7 +78,7 @@ def post_user_visible_apps(user_id: nil, app_ids: nil) end } - users_request_client.post("users/#{user_id}/relationships/visibleApps", body) + users_request_client.post("#{Version::V1}/users/#{user_id}/relationships/visibleApps", body) end # Replace app permissions for user @@ -88,7 +92,7 @@ def patch_user_visible_apps(user_id: nil, app_ids: nil) end } - users_request_client.patch("users/#{user_id}/relationships/visibleApps", body) + users_request_client.patch("#{Version::V1}/users/#{user_id}/relationships/visibleApps", body) end # Remove app permissions for user @@ -102,13 +106,13 @@ def delete_user_visible_apps(user_id: nil, app_ids: nil) end } params = nil - users_request_client.delete("users/#{user_id}/relationships/visibleApps", params, body) + users_request_client.delete("#{Version::V1}/users/#{user_id}/relationships/visibleApps", params, body) end # Get app permissions for user def get_user_visible_apps(user_id: id, limit: nil) params = users_request_client.build_params(filter: {}, includes: nil, limit: limit, sort: nil) - users_request_client.get("users/#{user_id}/visibleApps", params) + users_request_client.get("#{Version::V1}/users/#{user_id}/visibleApps", params) end # @@ -118,7 +122,7 @@ def get_user_visible_apps(user_id: id, limit: nil) # Get all invited users def get_user_invitations(filter: {}, includes: nil, limit: nil, sort: nil) params = users_request_client.build_params(filter: filter, includes: includes, limit: limit, sort: sort) - users_request_client.get("userInvitations", params) + users_request_client.get("#{Version::V1}/userInvitations", params) end # Invite new users to App Store Connect @@ -150,18 +154,18 @@ def post_user_invitation(email: nil, first_name: nil, last_name: nil, roles: [], # Avoid API error: You cannot set visible apps for this user because the user's roles give them access to all apps. body[:data].delete(:relationships) if all_apps_visible - users_request_client.post("userInvitations", body) + users_request_client.post("#{Version::V1}/userInvitations", body) end # Remove invited user from team (not yet accepted) def delete_user_invitation(user_invitation_id: nil) - users_request_client.delete("userInvitations/#{user_invitation_id}") + users_request_client.delete("#{Version::V1}/userInvitations/#{user_invitation_id}") end # Get all app permissions for invited user def get_user_invitation_visible_apps(user_invitation_id: id, limit: nil) params = users_request_client.build_params(filter: {}, includes: nil, limit: limit, sort: nil) - users_request_client.get("userInvitations/#{user_invitation_id}/visibleApps", params) + users_request_client.get("#{Version::V1}/userInvitations/#{user_invitation_id}/visibleApps", params) end end end diff --git a/spaceship/lib/spaceship/portal/certificate.rb b/spaceship/lib/spaceship/portal/certificate.rb index 90ac7541c0d..8cfcdfa1846 100644 --- a/spaceship/lib/spaceship/portal/certificate.rb +++ b/spaceship/lib/spaceship/portal/certificate.rb @@ -205,7 +205,7 @@ class MacProductionPush < PushCertificate; end # Class methods class << self - # Create a new code signing request that can be used to + # Create a new cert signing request that can be used to # generate a new certificate # @example # Create a new certificate signing request @@ -221,7 +221,7 @@ def create_certificate_signing_request ['CN', 'PEM', OpenSSL::ASN1::UTF8STRING] ]) csr.public_key = key.public_key - csr.sign(key, OpenSSL::Digest::SHA1.new) + csr.sign(key, OpenSSL::Digest::SHA256.new) return [csr, key] end diff --git a/spaceship/lib/spaceship/portal/provisioning_profile.rb b/spaceship/lib/spaceship/portal/provisioning_profile.rb index 033f66b87a4..56b495f2181 100644 --- a/spaceship/lib/spaceship/portal/provisioning_profile.rb +++ b/spaceship/lib/spaceship/portal/provisioning_profile.rb @@ -484,7 +484,14 @@ def certificate_valid? # @return (Bool) Is the current provisioning profile valid? # To also verify the certificate call certificate_valid? def valid? - return status == 'Active' + # Provisioning profiles are not invalidated automatically on the dev portal when the certificate expires. + # They become Invalid only when opened directly in the portal 🤷. + # We need to do an extra check on the expiration date to ensure the profile is valid. + expired = Time.now.utc > self.expires + + is_valid = status == 'Active' && !expired + + return is_valid end # @return (Bool) Is this profile managed by Xcode? diff --git a/spaceship/lib/spaceship/stats_middleware.rb b/spaceship/lib/spaceship/stats_middleware.rb index dc984ae1c78..d1881f0a48d 100644 --- a/spaceship/lib/spaceship/stats_middleware.rb +++ b/spaceship/lib/spaceship/stats_middleware.rb @@ -18,11 +18,11 @@ def services @services ||= [ ServiceOption.new("App Store Connect API (official)", "api.appstoreconnect.apple.com", "JWT"), ServiceOption.new("App Store Connect API (web session)", Spaceship::ConnectAPI::TestFlight::Client.hostname.gsub("https://", ""), "Web session"), - ServiceOption.new("App Store Connect API (web session)", Spaceship::ConnectAPI::Provisioning::Client.hostname.gsub("https://", ""), "Web session"), ServiceOption.new("Legacy iTunesConnect Auth", "idmsa.apple.com", "Web session"), ServiceOption.new("Legacy iTunesConnect Auth", "appstoreconnect.apple.com/olympus/v1/", "Web session"), ServiceOption.new("Legacy iTunesConnect", Spaceship::TunesClient.hostname.gsub("https://", ""), "Web session"), - ServiceOption.new("Legacy iTunesConnect Developer Portal", Spaceship::PortalClient.hostname.gsub("https://", ""), "Web session") + ServiceOption.new("Legacy iTunesConnect Developer Portal", Spaceship::PortalClient.hostname.gsub("https://", ""), "Web session"), + ServiceOption.new("App Store Connect API (web session)", Spaceship::ConnectAPI::Provisioning::Client.hostname.gsub("https://", ""), "Web session") ] end diff --git a/spaceship/lib/spaceship/tunes/app_submission.rb b/spaceship/lib/spaceship/tunes/app_submission.rb index a50759e6a0d..0b0e6c67a5c 100644 --- a/spaceship/lib/spaceship/tunes/app_submission.rb +++ b/spaceship/lib/spaceship/tunes/app_submission.rb @@ -22,11 +22,6 @@ class AppSubmission < TunesBase # To pass from the user - # @deprecated Set automatically by add_id_info_uses_idfa usage - # @return (Boolean) Ad ID Info - Limits ads tracking - # DEPRECATED: Use add_id_info_uses_idfa instead. - attr_accessor :add_id_info_limits_tracking - # @return (Boolean) Ad ID Info - Serves ads attr_accessor :add_id_info_serves_ads @@ -36,9 +31,6 @@ class AppSubmission < TunesBase # @return (Boolean) Ad ID Info - Tracks installs attr_accessor :add_id_info_tracks_install - # @return (Boolean) Ad ID Info - Uses idfa - attr_accessor :add_id_info_uses_idfa - # @return (Boolean) Content Rights - Contains third party content attr_accessor :content_rights_contains_third_party_content @@ -80,7 +72,6 @@ class AppSubmission < TunesBase 'adIdInfo.servesAds.value' => :add_id_info_serves_ads, 'adIdInfo.tracksAction.value' => :add_id_info_tracks_action, 'adIdInfo.tracksInstall.value' => :add_id_info_tracks_install, - 'adIdInfo.usesIdfa.value' => :add_id_info_uses_idfa, # Content Rights Section 'contentRights.containsThirdPartyContent.value' => :content_rights_contains_third_party_content, @@ -139,16 +130,6 @@ def complete! raw_data_clone.delete(:version) raw_data_clone.delete(:application) - # Check whether the application makes use of IDFA or not - # and automatically set the mandatory limitsTracking value in the request JSON accordingly. - if !self.add_id_info_uses_idfa.nil? && self.add_id_info_uses_idfa == true - # Application uses IDFA, before sending for submission limitsTracking key in the request JSON must be set to true (agreement). - raw_data_clone.set( - ["adIdInfo", "limitsTracking", "value"], - true - ) - end - client.send_app_submission(application.apple_id, application.edit_version(platform: platform).version_id, raw_data_clone) @submitted_for_review = true end diff --git a/spaceship/lib/spaceship/tunes/application.rb b/spaceship/lib/spaceship/tunes/application.rb index 0f745b54662..98e53d8f733 100644 --- a/spaceship/lib/spaceship/tunes/application.rb +++ b/spaceship/lib/spaceship/tunes/application.rb @@ -72,7 +72,7 @@ def all def find(identifier, mac: false) all.find do |app| ((app.apple_id && app.apple_id.casecmp(identifier.to_s) == 0) || (app.bundle_id && app.bundle_id.casecmp(identifier.to_s) == 0)) && - app.version_sets.any? { |v| (mac ? ["osx"] : ["ios", "appletvos"]).include?(v.platform) } + app.version_sets.any? { |v| (mac ? ["osx"] : ["ios", "xros", "appletvos"]).include?(v.platform) } end end diff --git a/spaceship/lib/spaceship/tunes/tunes_client.rb b/spaceship/lib/spaceship/tunes/tunes_client.rb index 297e62f7e47..8aa38495846 100644 --- a/spaceship/lib/spaceship/tunes/tunes_client.rb +++ b/spaceship/lib/spaceship/tunes/tunes_client.rb @@ -297,6 +297,8 @@ def applications "appletvos" when "MAC_OS" "osx" + when "VISION_OS" + "xros" when "IOS" "ios" else diff --git a/spaceship/spec/connect_api/fixtures/asc_individual_key.json b/spaceship/spec/connect_api/fixtures/asc_individual_key.json new file mode 100644 index 00000000000..61a0fb2cba5 --- /dev/null +++ b/spaceship/spec/connect_api/fixtures/asc_individual_key.json @@ -0,0 +1,4 @@ +{ + "key_id": "D485S484", + "key": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIOChdk7KmY4kakEbpQzG6h7/FpsLviYzsG8VIAY8dQNPoAoGCCqGSM49\nAwEHoUQDQgAEYrwrM8lljj4upX7lb4YwjsnrD9CDYrMqKRH34GHPc6mMSLmXPqFr\nTdWWgfn6fee6YcJhvdGZliO08CbpjMPY2Q==\n-----END EC PRIVATE KEY-----\n" +} diff --git a/spaceship/spec/connect_api/fixtures/testflight/apps.json b/spaceship/spec/connect_api/fixtures/testflight/apps.json index 452c7ffe459..721ba5e3773 100644 --- a/spaceship/spec/connect_api/fixtures/testflight/apps.json +++ b/spaceship/spec/connect_api/fixtures/testflight/apps.json @@ -1062,10 +1062,10 @@ "platform" : "IOS", "versionString" : "1.0", "appStoreState" : "PREPARE_FOR_SUBMISSION", + "appVersionState" : "PREPARE_FOR_SUBMISSION", "copyright" : null, "releaseType" : "AFTER_APPROVAL", "earliestReleaseDate" : null, - "usesIdfa" : null, "downloadable" : true, "createdDate" : "2022-07-18T11:17:21-07:00" }, @@ -1112,12 +1112,6 @@ "related" : "https://api.appstoreconnect.apple.com/v1/appStoreVersions/2c2e7526-bde0-4878-b04b-89d20dd2a692/appStoreVersionSubmission" } }, - "idfaDeclaration" : { - "links" : { - "self" : "https://api.appstoreconnect.apple.com/v1/appStoreVersions/2c2e7526-bde0-4878-b04b-89d20dd2a692/relationships/idfaDeclaration", - "related" : "https://api.appstoreconnect.apple.com/v1/appStoreVersions/2c2e7526-bde0-4878-b04b-89d20dd2a692/idfaDeclaration" - } - }, "appClipDefaultExperience" : { "links" : { "self" : "https://api.appstoreconnect.apple.com/v1/appStoreVersions/2c2e7526-bde0-4878-b04b-89d20dd2a692/relationships/appClipDefaultExperience", @@ -1147,10 +1141,10 @@ "platform" : "IOS", "versionString" : "1.0", "appStoreState" : "PREPARE_FOR_SUBMISSION", + "appVersionState" : "PREPARE_FOR_SUBMISSION", "copyright" : null, "releaseType" : "AFTER_APPROVAL", "earliestReleaseDate" : null, - "usesIdfa" : null, "downloadable" : true, "createdDate" : "2022-06-14T19:38:26-07:00" }, @@ -1197,12 +1191,6 @@ "related" : "https://api.appstoreconnect.apple.com/v1/appStoreVersions/9e70d45f-bfc2-43a6-ab98-09fad49459f1/appStoreVersionSubmission" } }, - "idfaDeclaration" : { - "links" : { - "self" : "https://api.appstoreconnect.apple.com/v1/appStoreVersions/9e70d45f-bfc2-43a6-ab98-09fad49459f1/relationships/idfaDeclaration", - "related" : "https://api.appstoreconnect.apple.com/v1/appStoreVersions/9e70d45f-bfc2-43a6-ab98-09fad49459f1/idfaDeclaration" - } - }, "appClipDefaultExperience" : { "links" : { "self" : "https://api.appstoreconnect.apple.com/v1/appStoreVersions/9e70d45f-bfc2-43a6-ab98-09fad49459f1/relationships/appClipDefaultExperience", diff --git a/spaceship/spec/connect_api/fixtures/testflight/beta_testers.json b/spaceship/spec/connect_api/fixtures/testflight/beta_testers.json index 500ba8a5d4a..cbfc1b1c780 100644 --- a/spaceship/spec/connect_api/fixtures/testflight/beta_testers.json +++ b/spaceship/spec/connect_api/fixtures/testflight/beta_testers.json @@ -7,7 +7,27 @@ "lastName" : "AreCute", "email" : "email@email.com", "inviteType" : "EMAIL", - "invitation" : null + "isDeleted": false, + "betaTesterState": "INSTALLED", + "lastModifiedDate": "2024-01-21T20:52:18.921-08:00", + "installedCfBundleShortVersionString": "1.3.300", + "installedCfBundleVersion": "1113", + "removeAfterDate": "2024-04-20T00:00:00-07:00", + "latestExpiringCfBundleShortVersionString": "1.3.300", + "latestExpiringCfBundleVersionString": "1113", + "installedDevice": "iPhone14_7", + "installedOsVersion": "17.2.1", + "installedDevicePlatform": "IOS", + "latestInstalledDevice": "iPhone14_7", + "latestInstalledOsVersion": "17.2.1", + "latestInstalledDevicePlatform": "IOS", + "numberOfInstalledDevices": 1.0, + "appDevices": [{ + "model": "iPhone14_7", + "platform": "IOS", + "osVersion": "17.2.1", + "appBuildVersion": "1.3.300 (1113)" + }] }, "relationships" : { "apps" : { diff --git a/spaceship/spec/connect_api/fixtures/tunes/app_availabilities_ready_for_distribution.json b/spaceship/spec/connect_api/fixtures/tunes/app_availabilities_ready_for_distribution.json new file mode 100644 index 00000000000..7da106dad52 --- /dev/null +++ b/spaceship/spec/connect_api/fixtures/tunes/app_availabilities_ready_for_distribution.json @@ -0,0 +1,73 @@ +{ + "data": { + "type": "appAvailabilities", + "id": "123456789", + "attributes": { + "availableInNewTerritories": false + }, + "relationships": { + "territoryAvailabilities": { + "meta": { + "paging": { + "total": 175, + "limit": 200 + } + }, + "data": [ + { + "type": "territoryAvailabilities", + "id": "abcdefg" + }, + { + "type": "territoryAvailabilities", + "id": "hijklmn" + } + ], + "links": { + "self": "https://appstoreconnect.apple.com/iris/v2/appAvailabilities/123456789/relationships/territoryAvailabilities", + "related": "https://appstoreconnect.apple.com/iris/v2/appAvailabilities/123456789/territoryAvailabilities" + } + } + }, + "links": { + "self": "https://appstoreconnect.apple.com/iris/v2/appAvailabilities/123456789" + } + }, + "included": [ + { + "type": "territoryAvailabilities", + "id": "abcdefg", + "attributes": { + "available": true, + "availableInPast": true, + "preOrderAvailableInPast": false, + "releaseDate": "2024-02-26", + "preOrderEnabled": false, + "preOrderPublishDate": null, + "contentStatuses": ["AVAILABLE"] + }, + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/territoryAvailabilities/abcdefg" + } + }, + { + "type": "territoryAvailabilities", + "id": "hijklmn", + "attributes": { + "available": true, + "availableInPast": true, + "preOrderAvailableInPast": false, + "releaseDate": "2024-02-26", + "preOrderEnabled": false, + "preOrderPublishDate": null, + "contentStatuses": ["AVAILABLE"] + }, + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/territoryAvailabilities/hijklmn" + } + } + ], + "links": { + "self": "https://appstoreconnect.apple.com/iris/v2/appAvailabilities/123456789?include=territoryAvailabilities" + } +} diff --git a/spaceship/spec/connect_api/fixtures/tunes/app_availabilities_removed_app.json b/spaceship/spec/connect_api/fixtures/tunes/app_availabilities_removed_app.json new file mode 100644 index 00000000000..a79ed8f94cb --- /dev/null +++ b/spaceship/spec/connect_api/fixtures/tunes/app_availabilities_removed_app.json @@ -0,0 +1,73 @@ +{ + "data": { + "type": "appAvailabilities", + "id": "123456789", + "attributes": { + "availableInNewTerritories": false + }, + "relationships": { + "territoryAvailabilities": { + "meta": { + "paging": { + "total": 175, + "limit": 200 + } + }, + "data": [ + { + "type": "territoryAvailabilities", + "id": "abcdefg" + }, + { + "type": "territoryAvailabilities", + "id": "hijklmn" + } + ], + "links": { + "self": "https://appstoreconnect.apple.com/iris/v2/appAvailabilities/123456789/relationships/territoryAvailabilities", + "related": "https://appstoreconnect.apple.com/iris/v2/appAvailabilities/123456789/territoryAvailabilities" + } + } + }, + "links": { + "self": "https://appstoreconnect.apple.com/iris/v2/appAvailabilities/123456789" + } + }, + "included": [ + { + "type": "territoryAvailabilities", + "id": "abcdefg", + "attributes": { + "available": false, + "availableInPast": true, + "preOrderAvailableInPast": false, + "releaseDate": "2024-02-26", + "preOrderEnabled": false, + "preOrderPublishDate": null, + "contentStatuses": ["CANNOT_SELL"] + }, + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/territoryAvailabilities/abcdefg" + } + }, + { + "type": "territoryAvailabilities", + "id": "hijklmn", + "attributes": { + "available": false, + "availableInPast": true, + "preOrderAvailableInPast": false, + "releaseDate": "2024-02-26", + "preOrderEnabled": false, + "preOrderPublishDate": null, + "contentStatuses": ["CANNOT_SELL"] + }, + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/territoryAvailabilities/hijklmn" + } + } + ], + "links": { + "self": "https://appstoreconnect.apple.com/iris/v2/appAvailabilities/123456789?include=territoryAvailabilities" + } +} diff --git a/spaceship/spec/connect_api/fixtures/tunes/app_infos.json b/spaceship/spec/connect_api/fixtures/tunes/app_infos.json new file mode 100644 index 00000000000..805b0c46081 --- /dev/null +++ b/spaceship/spec/connect_api/fixtures/tunes/app_infos.json @@ -0,0 +1,137 @@ +{ + "data": [ + { + "type": "appInfos", + "id": "1111-11111", + "attributes": { + "appStoreState": "READY_FOR_SALE", + "state": "READY_FOR_DISTRIBUTION", + "appStoreAgeRating": "FOUR_PLUS", + "brazilAgeRating": "L", + "brazilAgeRatingV2": "SELF_RATED_L", + "kidsAgeBand": null + }, + "relationships": { + "ageRatingDeclaration": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/relationships/ageRatingDeclaration", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/ageRatingDeclaration" + } + }, + "appInfoLocalizations": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/relationships/appInfoLocalizations", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/appInfoLocalizations" + } + }, + "primaryCategory": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/relationships/primaryCategory", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/primaryCategory" + } + }, + "primarySubcategoryOne": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/relationships/primarySubcategoryOne", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/primarySubcategoryOne" + } + }, + "primarySubcategoryTwo": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/relationships/primarySubcategoryTwo", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/primarySubcategoryTwo" + } + }, + "secondaryCategory": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/relationships/secondaryCategory", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/secondaryCategory" + } + }, + "secondarySubcategoryOne": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/relationships/secondarySubcategoryOne", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/secondarySubcategoryOne" + } + }, + "secondarySubcategoryTwo": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/relationships/secondarySubcategoryTwo", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111/secondarySubcategoryTwo" + } + } + }, + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-11111" + } + }, + { + "type": "appInfos", + "id": "1111-2222", + "attributes": { + "appStoreState": "PREPARE_FOR_SUBMISSION", + "state": "PREPARE_FOR_SUBMISSION", + "appStoreAgeRating": "FOUR_PLUS", + "brazilAgeRating": "L", + "brazilAgeRatingV2": "SELF_RATED_L", + "kidsAgeBand": null + }, + "relationships": { + "ageRatingDeclaration": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/relationships/ageRatingDeclaration", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/ageRatingDeclaration" + } + }, + "appInfoLocalizations": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/relationships/appInfoLocalizations", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/appInfoLocalizations" + } + }, + "primaryCategory": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/relationships/primaryCategory", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/primaryCategory" + } + }, + "primarySubcategoryOne": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/relationships/primarySubcategoryOne", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/primarySubcategoryOne" + } + }, + "primarySubcategoryTwo": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/relationships/primarySubcategoryTwo", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/primarySubcategoryTwo" + } + }, + "secondaryCategory": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/relationships/secondaryCategory", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/secondaryCategory" + } + }, + "secondarySubcategoryOne": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/relationships/secondarySubcategoryOne", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/secondarySubcategoryOne" + } + }, + "secondarySubcategoryTwo": { + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/relationships/secondarySubcategoryTwo", + "related": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222/secondarySubcategoryTwo" + } + } + }, + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/appInfos/1111-2222" + } + } + ], + "links": { + "self": "https://appstoreconnect.apple.com/iris/v1/apps/123456789/appInfos" + } +} diff --git a/spaceship/spec/connect_api/fixtures/tunes/review_submission.json b/spaceship/spec/connect_api/fixtures/tunes/review_submission.json index ab8ca6c9997..aa99c373a24 100644 --- a/spaceship/spec/connect_api/fixtures/tunes/review_submission.json +++ b/spaceship/spec/connect_api/fixtures/tunes/review_submission.json @@ -49,7 +49,6 @@ "copyright" : "ACME Apps 2021", "downloadable" : true, "earliestReleaseDate" : null, - "usesIdfa" : true, "versionString" : "1.0.1", "promotedDate" : null, "platform" : "IOS", @@ -61,7 +60,8 @@ "isWatchOnly" : false, "createdDate" : "2021-01-01T10:10:10-07:00", "releaseType" : "MANUAL", - "appStoreState" : "READY_FOR_REVIEW" + "appStoreState" : "READY_FOR_REVIEW", + "appVersionState" : "READY_FOR_REVIEW" }, "relationships" : { "appStoreVersionLocalizations" : { @@ -70,12 +70,6 @@ "related" : "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/123456789-app-store-version/appStoreVersionLocalizations" } }, - "idfaDeclaration" : { - "links" : { - "self" : "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/123456789-app-store-version/relationships/idfaDeclaration", - "related" : "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/123456789-app-store-version/idfaDeclaration" - } - }, "appStoreReviewDetail" : { "links" : { "self" : "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/123456789-app-store-version/relationships/appStoreReviewDetail", diff --git a/spaceship/spec/connect_api/fixtures/tunes/review_submission_items.json b/spaceship/spec/connect_api/fixtures/tunes/review_submission_items.json index a48d9ace04d..bd95c01aa41 100644 --- a/spaceship/spec/connect_api/fixtures/tunes/review_submission_items.json +++ b/spaceship/spec/connect_api/fixtures/tunes/review_submission_items.json @@ -34,7 +34,6 @@ "copyright" : "ACME Apps 2021", "downloadable" : true, "earliestReleaseDate" : null, - "usesIdfa" : true, "versionString" : "1.0.1", "promotedDate" : null, "platform" : "IOS", @@ -46,7 +45,8 @@ "isWatchOnly" : false, "createdDate" : "2021-01-01T10:10:10-07:00", "releaseType" : "MANUAL", - "appStoreState" : "READY_FOR_REVIEW" + "appStoreState" : "READY_FOR_REVIEW", + "appVersionState" : "READY_FOR_REVIEW" }, "relationships" : { "appStoreVersionLocalizations" : { @@ -55,12 +55,6 @@ "related" : "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/123456789-app-store-version/appStoreVersionLocalizations" } }, - "idfaDeclaration" : { - "links" : { - "self" : "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/123456789-app-store-version/relationships/idfaDeclaration", - "related" : "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/123456789-app-store-version/idfaDeclaration" - } - }, "appStoreReviewDetail" : { "links" : { "self" : "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/123456789-app-store-version/relationships/appStoreReviewDetail", diff --git a/spaceship/spec/connect_api/models/app_spec.rb b/spaceship/spec/connect_api/models/app_spec.rb index 54f1131fd47..ef0327bd055 100644 --- a/spaceship/spec/connect_api/models/app_spec.rb +++ b/spaceship/spec/connect_api/models/app_spec.rb @@ -86,6 +86,28 @@ expect(model.bundle_id).to eq("com.joshholtz.FastlaneTest") end + it('fetches live app info') do + ConnectAPIStubbing::Tunes.stub_get_app_infos + app = Spaceship::ConnectAPI::App.new("123456789", []) + + info = app.fetch_live_app_info(includes: nil) + expect(info.id).to eq("1111-11111") + expect(info.app_store_age_rating).to eq("FOUR_PLUS") + expect(info.app_store_state).to eq("READY_FOR_SALE") + expect(info.state).to eq("READY_FOR_DISTRIBUTION") + end + + it('fetches edit app info') do + ConnectAPIStubbing::Tunes.stub_get_app_infos + app = Spaceship::ConnectAPI::App.new("123456789", []) + + info = app.fetch_edit_app_info(includes: nil) + expect(info.id).to eq("1111-2222") + expect(info.app_store_age_rating).to eq("FOUR_PLUS") + expect(info.app_store_state).to eq("PREPARE_FOR_SUBMISSION") + expect(info.state).to eq("PREPARE_FOR_SUBMISSION") + end + it 'creates beta group' do app = Spaceship::ConnectAPI::App.find("com.joshholtz.FastlaneTest") @@ -135,4 +157,34 @@ expect(review_submission.state).to eq(Spaceship::ConnectAPI::ReviewSubmission::ReviewSubmissionState::READY_FOR_REVIEW) end end + + describe("#get_app_availabilities") do + it('gets app availabilities when app is ready for distribution') do + ConnectAPIStubbing::Tunes.stub_get_app_availabilities_ready_for_distribution + app = Spaceship::ConnectAPI::App.new("123456789", []) + + availabilities = app.get_app_availabilities + + expect(availabilities.availableInNewTerritories).to be(false) + expect(availabilities.territory_availabilities.count).to eq(2) + expect(availabilities.territory_availabilities[0].available).to be(true) + expect(availabilities.territory_availabilities[1].available).to be(true) + expect(availabilities.territory_availabilities[0].contentStatuses).to eq(["AVAILABLE"]) + expect(availabilities.territory_availabilities[1].contentStatuses).to eq(["AVAILABLE"]) + end + + it('gets app availabilities when app is removed from sale') do + ConnectAPIStubbing::Tunes.stub_get_app_availabilities_removed_from_sale + app = Spaceship::ConnectAPI::App.new("123456789", []) + + availabilities = app.get_app_availabilities + + expect(availabilities.availableInNewTerritories).to be(false) + expect(availabilities.territory_availabilities.count).to eq(2) + expect(availabilities.territory_availabilities[0].available).to be(false) + expect(availabilities.territory_availabilities[1].available).to be(false) + expect(availabilities.territory_availabilities[0].contentStatuses).to eq(["CANNOT_SELL"]) + expect(availabilities.territory_availabilities[1].contentStatuses).to eq(["CANNOT_SELL"]) + end + end end diff --git a/spaceship/spec/connect_api/models/beta_tester_spec.rb b/spaceship/spec/connect_api/models/beta_tester_spec.rb index 46425da1a92..0edfcafa04e 100644 --- a/spaceship/spec/connect_api/models/beta_tester_spec.rb +++ b/spaceship/spec/connect_api/models/beta_tester_spec.rb @@ -17,7 +17,21 @@ expect(model.last_name).to eq("AreCute") expect(model.email).to eq("email@email.com") expect(model.invite_type).to eq("EMAIL") - expect(model.invitation).to eq(nil) + expect(model.beta_tester_state).to eq("INSTALLED") + expect(model.is_deleted).to eq(false) + expect(model.last_modified_date).to eq("2024-01-21T20:52:18.921-08:00") + expect(model.installed_cf_bundle_short_version_string).to eq("1.3.300") + expect(model.installed_cf_bundle_version).to eq("1113") + expect(model.remove_after_date).to eq("2024-04-20T00:00:00-07:00") + expect(model.installed_device).to eq("iPhone14_7") + expect(model.installed_os_version).to eq("17.2.1") + expect(model.number_of_installed_devices).to eq(1.0) + expect(model.latest_expiring_cf_bundle_short_version_string).to eq("1.3.300") + expect(model.latest_expiring_cf_bundle_version_string).to eq("1113") + expect(model.installed_device_platform).to eq("IOS") + expect(model.latest_installed_device).to eq("iPhone14_7") + expect(model.latest_installed_os_version).to eq("17.2.1") + expect(model.latest_installed_device_platform).to eq("IOS") end end end diff --git a/spaceship/spec/connect_api/provisioning/provisioning_client_spec.rb b/spaceship/spec/connect_api/provisioning/provisioning_client_spec.rb index c309e1e8abf..bc996a8d4f7 100644 --- a/spaceship/spec/connect_api/provisioning/provisioning_client_spec.rb +++ b/spaceship/spec/connect_api/provisioning/provisioning_client_spec.rb @@ -11,6 +11,7 @@ context 'sends api request' do before(:each) do allow(client).to receive(:handle_response) + allow(client).to receive(:team_id).and_return("XXXXXXXXXX") end def test_request_params(url, params) @@ -47,7 +48,7 @@ def test_request_body(url, body) describe "bundleIds" do context 'get_bundle_ids' do - let(:path) { "bundleIds" } + let(:path) { "v1/bundleIds" } it 'succeeds' do params = {} @@ -61,49 +62,49 @@ def test_request_body(url, body) describe "bundleId Capability" do context 'patch_bundle_id_capability' do it 'should make a request to turn APP_ATTEST on' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::APP_ATTEST) + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::APP_ATTEST) end it 'should make a request to turn APP_ATTEST off' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: false, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::APP_ATTEST) + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: false, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::APP_ATTEST) end it 'should make a request to turn ACCESS_WIFI on' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::ACCESS_WIFI_INFORMATION) + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::ACCESS_WIFI_INFORMATION) end it 'should make a request to turn ACCESS_WIFI off' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: false, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::ACCESS_WIFI_INFORMATION) + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: false, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::ACCESS_WIFI_INFORMATION) end it 'should make a request to turn DATA_PROTECTION complete' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::DATA_PROTECTION, + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::DATA_PROTECTION, settings: settings = [{ key: Spaceship::ConnectAPI::BundleIdCapability::Settings::DATA_PROTECTION_PERMISSION_LEVEL, options: [ { key: Spaceship::ConnectAPI::BundleIdCapability::Options::COMPLETE_PROTECTION } ] }]) end it 'should make a request to turn DATA_PROTECTION unless_open' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::DATA_PROTECTION, + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::DATA_PROTECTION, settings: settings = [{ key: Spaceship::ConnectAPI::BundleIdCapability::Settings::DATA_PROTECTION_PERMISSION_LEVEL, options: [ { key: Spaceship::ConnectAPI::BundleIdCapability::Options::PROTECTED_UNLESS_OPEN } ] }]) end it 'should make a request to turn DATA_PROTECTION until_first_auth' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::DATA_PROTECTION, + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::DATA_PROTECTION, settings: settings = [{ key: Spaceship::ConnectAPI::BundleIdCapability::Settings::DATA_PROTECTION_PERMISSION_LEVEL, options: [ { key: Spaceship::ConnectAPI::BundleIdCapability::Options::PROTECTED_UNTIL_FIRST_USER_AUTH } ] }]) end it 'should make a request to turn DATA_PROTECTION off' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: false, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::DATA_PROTECTION) + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: false, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::DATA_PROTECTION) end it 'should make a request to turn ICLOUD xcode6_compatible' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::ICLOUD, + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::ICLOUD, settings: settings = [{ key: Spaceship::ConnectAPI::BundleIdCapability::Settings::ICLOUD_VERSION, options: [ { key: Spaceship::ConnectAPI::BundleIdCapability::Options::XCODE_5 } ] }]) end it 'should make a request to turn ICLOUD xcode5_compatible' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::ICLOUD, + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: true, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::ICLOUD, settings: settings = [{ key: Spaceship::ConnectAPI::BundleIdCapability::Settings::ICLOUD_VERSION, options: [ { key: Spaceship::ConnectAPI::BundleIdCapability::Options::XCODE_6 } ] }]) end it 'should make a request to turn ICLOUD off' do - client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "XXXXXXXXXX", enabled: false, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::ICLOUD) + client.patch_bundle_id_capability(bundle_id_id: "ABCD1234", seed_id: "SEEDID", enabled: false, capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::ICLOUD) end end end describe "certificates" do context 'get_certificates' do - let(:path) { "certificates" } + let(:path) { "v1/certificates" } it 'succeeds' do params = {} @@ -114,7 +115,7 @@ def test_request_body(url, body) end context 'get_certificates_for_profile' do - let(:path) { "profiles/123456789/certificates" } + let(:path) { "v1/profiles/123456789/certificates" } it 'succeeds' do params = {} @@ -127,7 +128,7 @@ def test_request_body(url, body) describe "devices" do context 'get_devices' do - let(:path) { "devices" } + let(:path) { "v1/devices" } it 'succeeds' do params = {} @@ -140,7 +141,7 @@ def test_request_body(url, body) describe "profiles" do context 'get_profiles' do - let(:path) { "profiles" } + let(:path) { "v1/profiles" } it 'succeeds' do params = {} diff --git a/spaceship/spec/connect_api/provisioning/provisioning_stubbing.rb b/spaceship/spec/connect_api/provisioning/provisioning_stubbing.rb index c62787769f4..5adec3fb5e5 100644 --- a/spaceship/spec/connect_api/provisioning/provisioning_stubbing.rb +++ b/spaceship/spec/connect_api/provisioning/provisioning_stubbing.rb @@ -22,53 +22,53 @@ def stub_bundle_ids def stub_patch_bundle_id_capability # APP_ATTEST stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => false, "settings" => [] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "APP_ATTEST" } } } }] } } } }). to_return(status: 200) stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => true, "settings" => [] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "APP_ATTEST" } } } }] } } } }). to_return(status: 200) # ACCESS_WIFI stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => false, "settings" => [] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "ACCESS_WIFI_INFORMATION" } } } }] } } } }). to_return(status: 200) stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => true, "settings" => [] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "ACCESS_WIFI_INFORMATION" } } } }] } } } }). to_return(status: 200) # DATA_PROTECTION stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => true, "settings" => [{ "key" => "DATA_PROTECTION_PERMISSION_LEVEL", "options" => [{ "key" => "COMPLETE_PROTECTION" }] }] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "DATA_PROTECTION" } } } }] } } } }).to_return(status: 200) stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => true, "settings" => [{ "key" => "DATA_PROTECTION_PERMISSION_LEVEL", "options" => [{ "key" => "PROTECTED_UNLESS_OPEN" }] }] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "DATA_PROTECTION" } } } }] } } } }).to_return(status: 200) stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => true, "settings" => [{ "key" => "DATA_PROTECTION_PERMISSION_LEVEL", "options" => [{ "key" => "PROTECTED_UNTIL_FIRST_USER_AUTH" }] }] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "DATA_PROTECTION" } } } }] } } } }).to_return(status: 200) stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => false, "settings" => [] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "DATA_PROTECTION" } } } }] } } } }). to_return(status: 200) # ICLOUD stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => true, "settings" => [{ "key" => "ICLOUD_VERSION", "options" => [{ "key" => "XCODE_5" }] }] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "ICLOUD" } } } }] } } } }). to_return(status: 200) stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => true, "settings" => [{ "key" => "ICLOUD_VERSION", "options" => [{ "key" => "XCODE_6" }] }] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "ICLOUD" } } } }] } } } }). to_return(status: 200) stub_request(:patch, "https://developer.apple.com/services-account/v1/bundleIds/ABCD1234"). - with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "teamId" => "XXXXXXXXXX" }, + with(body: { "data" => { "type" => "bundleIds", "id" => "ABCD1234", "attributes" => { "permissions" => { "edit" => true, "delete" => true }, "seedId" => "SEEDID", "teamId" => "XXXXXXXXXX" }, "relationships" => { "bundleIdCapabilities" => { "data" => [{ "type" => "bundleIdCapabilities", "attributes" => { "enabled" => false, "settings" => [] }, "relationships" => { "capability" => { "data" => { "type" => "capabilities", "id" => "ICLOUD" } } } }] } } } }). to_return(status: 200) end diff --git a/spaceship/spec/connect_api/testflight/testflight_client_spec.rb b/spaceship/spec/connect_api/testflight/testflight_client_spec.rb index b93c27a1df8..84988a250de 100644 --- a/spaceship/spec/connect_api/testflight/testflight_client_spec.rb +++ b/spaceship/spec/connect_api/testflight/testflight_client_spec.rb @@ -47,7 +47,7 @@ def test_request_body(url, body) describe "apps" do context 'get_apps' do - let(:path) { "apps" } + let(:path) { "v1/apps" } let(:bundle_id) { "com.bundle.id" } it 'succeeds' do @@ -67,7 +67,7 @@ def test_request_body(url, body) context 'get_app' do let(:app_id) { "123456789" } - let(:path) { "apps/#{app_id}" } + let(:path) { "v1/apps/#{app_id}" } it 'succeeds' do params = {} @@ -80,7 +80,7 @@ def test_request_body(url, body) describe "betaAppLocalizations" do context 'get_beta_app_localizations' do - let(:path) { "betaAppLocalizations" } + let(:path) { "v1/betaAppLocalizations" } let(:app_id) { "123" } it 'succeeds' do @@ -99,7 +99,7 @@ def test_request_body(url, body) end context 'post_beta_app_localizations' do - let(:path) { "betaAppLocalizations" } + let(:path) { "v1/betaAppLocalizations" } let(:app_id) { "123" } let(:attributes) { { key: "value" } } let(:body) do @@ -129,7 +129,7 @@ def test_request_body(url, body) end context 'patch_beta_app_localizations' do - let(:path) { "betaAppLocalizations" } + let(:path) { "v1/betaAppLocalizations" } let(:localization_id) { "123" } let(:attributes) { { key: "value" } } let(:body) do @@ -154,7 +154,7 @@ def test_request_body(url, body) describe "betaAppReviewDetails" do context 'get_beta_app_review_detail' do - let(:path) { "betaAppReviewDetails" } + let(:path) { "v1/betaAppReviewDetails" } let(:app_id) { "123" } it 'succeeds' do @@ -173,7 +173,7 @@ def test_request_body(url, body) end context 'patch_beta_app_review_detail' do - let(:path) { "betaAppReviewDetails" } + let(:path) { "v1/betaAppReviewDetails" } let(:app_id) { "123" } let(:attributes) { { key: "value" } } let(:body) do @@ -197,7 +197,7 @@ def test_request_body(url, body) describe "betaAppReviewSubmissions" do context 'get_beta_app_review_submissions' do - let(:path) { "betaAppReviewSubmissions" } + let(:path) { "v1/betaAppReviewSubmissions" } let(:app_id) { "123" } it 'succeeds' do @@ -216,7 +216,7 @@ def test_request_body(url, body) end context 'post_beta_app_review_submissions' do - let(:path) { "betaAppReviewSubmissions" } + let(:path) { "v1/betaAppReviewSubmissions" } let(:build_id) { "123" } let(:body) do { @@ -245,7 +245,7 @@ def test_request_body(url, body) context 'delete_beta_app_review_submission' do let(:beta_app_review_submission_id) { "123" } - let(:path) { "betaAppReviewSubmissions/#{beta_app_review_submission_id}" } + let(:path) { "v1/betaAppReviewSubmissions/#{beta_app_review_submission_id}" } it 'succeeds' do params = {} @@ -258,7 +258,7 @@ def test_request_body(url, body) describe "betaBuildLocalizations" do context 'get_beta_build_localizations' do - let(:path) { "betaBuildLocalizations" } + let(:path) { "v1/betaBuildLocalizations" } let(:build_id) { "123" } it 'succeeds' do @@ -277,7 +277,7 @@ def test_request_body(url, body) end context 'post_beta_build_localizations' do - let(:path) { "betaBuildLocalizations" } + let(:path) { "v1/betaBuildLocalizations" } let(:build_id) { "123" } let(:attributes) { { key: "value" } } let(:body) do @@ -307,7 +307,7 @@ def test_request_body(url, body) end context 'patch_beta_build_localizations' do - let(:path) { "betaBuildLocalizations" } + let(:path) { "v1/betaBuildLocalizations" } let(:localization_id) { "123" } let(:attributes) { { key: "value" } } let(:body) do @@ -332,7 +332,7 @@ def test_request_body(url, body) describe "betaFeedbacks" do context 'get_beta_feedbacks' do - let(:path) { "betaFeedbacks" } + let(:path) { "v1/betaFeedbacks" } let(:app_id) { "123456789" } let(:default_params) { {} } @@ -354,7 +354,7 @@ def test_request_body(url, body) describe "betaGroups" do context 'get_beta_groups' do - let(:path) { "betaGroups" } + let(:path) { "v1/betaGroups" } let(:name) { "sir group a lot" } let(:default_params) { {} } @@ -374,7 +374,7 @@ def test_request_body(url, body) end context 'add_beta_groups_to_build' do - let(:path) { "builds" } + let(:path) { "v1/builds" } let(:build_id) { "123" } let(:beta_group_ids) { ["123", "456"] } let(:body) do @@ -398,7 +398,7 @@ def test_request_body(url, body) end context 'delete_beta_groups_from_build' do - let(:path) { "builds" } + let(:path) { "v1/builds" } let(:build_id) { "123" } let(:beta_group_ids) { ["123", "456"] } let(:body) do @@ -422,7 +422,7 @@ def test_request_body(url, body) end context 'patch_beta_groups' do - let(:path) { "betaGroups" } + let(:path) { "v1/betaGroups" } let(:beta_group_id) { "123" } let(:attributes) { { public_link_enabled: false } } let(:body) do @@ -447,7 +447,7 @@ def test_request_body(url, body) describe "betaTesters" do context 'get_beta_testers' do - let(:path) { "betaTesters" } + let(:path) { "v1/betaTesters" } let(:app_id) { "123" } it 'succeeds' do @@ -466,7 +466,7 @@ def test_request_body(url, body) end context 'post_bulk_beta_tester_assignments' do - let(:path) { "bulkBetaTesterAssignments" } + let(:path) { "v1/bulkBetaTesterAssignments" } let(:beta_group_id) { "123" } let(:beta_testers) do [ @@ -503,7 +503,7 @@ def test_request_body(url, body) end context 'post_beta_tester_assignment' do - let(:path) { "betaTesters" } + let(:path) { "v1/betaTesters" } let(:beta_group_ids) { ["123", "456"] } let(:attributes) { { email: "email1", firstName: "first1", lastName: "last1" } } let(:body) do @@ -537,7 +537,7 @@ def test_request_body(url, body) context "add_beta_tester_to_group" do let(:beta_group_id) { "123" } let(:beta_tester_ids) { ["1234", "5678"] } - let(:path) { "betaGroups/#{beta_group_id}/relationships/betaTesters" } + let(:path) { "v1/betaGroups/#{beta_group_id}/relationships/betaTesters" } let(:body) do { data: beta_tester_ids.map do |id| @@ -561,7 +561,7 @@ def test_request_body(url, body) context 'delete_beta_tester_from_apps' do let(:beta_tester_id) { "123" } let(:app_ids) { ["1234", "5678"] } - let(:path) { "betaTesters/#{beta_tester_id}/relationships/apps" } + let(:path) { "v1/betaTesters/#{beta_tester_id}/relationships/apps" } let(:body) do { data: app_ids.map do |id| @@ -585,7 +585,7 @@ def test_request_body(url, body) context 'delete_beta_tester_from_beta_groups' do let(:beta_tester_id) { "123" } let(:beta_group_ids) { ["1234", "5678"] } - let(:path) { "betaTesters/#{beta_tester_id}/relationships/betaGroups" } + let(:path) { "v1/betaTesters/#{beta_tester_id}/relationships/betaGroups" } let(:body) do { data: beta_group_ids.map do |id| @@ -609,7 +609,7 @@ def test_request_body(url, body) context "delete_beta_testers_from_app" do let(:app_id) { "123" } let(:beta_tester_ids) { ["1234", "5678"] } - let(:path) { "apps/#{app_id}/relationships/betaTesters" } + let(:path) { "v1/apps/#{app_id}/relationships/betaTesters" } let(:body) do { data: beta_tester_ids.map do |id| @@ -632,7 +632,7 @@ def test_request_body(url, body) context 'add_beta_tester_to_builds' do let(:beta_tester_id) { "123" } let(:build_ids) { ["1234", "5678"] } - let(:path) { "betaTesters/#{beta_tester_id}/relationships/builds" } + let(:path) { "v1/betaTesters/#{beta_tester_id}/relationships/builds" } let(:body) do { data: build_ids.map do |id| @@ -654,7 +654,7 @@ def test_request_body(url, body) end context 'add_beta_testers_to_build' do - let(:path) { "builds" } + let(:path) { "v1/builds" } let(:build_id) { "123" } let(:beta_tester_ids) { ["123", "456"] } let(:body) do @@ -678,7 +678,7 @@ def test_request_body(url, body) end context 'delete_beta_testers_from_build' do - let(:path) { "builds" } + let(:path) { "v1/builds" } let(:build_id) { "123" } let(:beta_tester_ids) { ["123", "456"] } let(:body) do @@ -704,7 +704,7 @@ def test_request_body(url, body) describe "builds" do context 'get_builds' do - let(:path) { "builds" } + let(:path) { "v1/builds" } let(:build_id) { "123" } let(:default_params) { { include: "buildBetaDetail,betaBuildMetrics", limit: 10, sort: "uploadedDate" } } @@ -725,7 +725,7 @@ def test_request_body(url, body) context 'get_build' do let(:build_id) { "123" } - let(:path) { "builds/#{build_id}" } + let(:path) { "v1/builds/#{build_id}" } it 'succeeds' do params = {} @@ -736,7 +736,7 @@ def test_request_body(url, body) end context 'patch_builds' do - let(:path) { "builds" } + let(:path) { "v1/builds" } let(:build_id) { "123" } let(:attributes) { { name: "some_name" } } let(:body) do @@ -761,7 +761,7 @@ def test_request_body(url, body) describe "buildBetaDetails" do context 'get_build_beta_details' do - let(:path) { "buildBetaDetails" } + let(:path) { "v1/buildBetaDetails" } let(:build_id) { "123" } it 'succeeds' do @@ -780,7 +780,7 @@ def test_request_body(url, body) end context 'patch_build_beta_details' do - let(:path) { "buildBetaDetails" } + let(:path) { "v1/buildBetaDetails" } let(:build_beta_details_id) { "123" } let(:attributes) { { key: "value" } } let(:body) do @@ -806,7 +806,7 @@ def test_request_body(url, body) describe "buildDeliveries" do context 'get_build_deliveries' do let(:app_id) { "123" } - let(:path) { "apps/#{app_id}/buildDeliveries" } + let(:path) { "v1/apps/#{app_id}/buildDeliveries" } let(:version) { "189" } let(:default_params) { {} } @@ -828,7 +828,7 @@ def test_request_body(url, body) describe "preReleaseVersions" do context 'get_pre_release_versions' do - let(:path) { "preReleaseVersions" } + let(:path) { "v1/preReleaseVersions" } let(:version) { "186" } let(:default_params) { {} } diff --git a/spaceship/spec/connect_api/token_spec.rb b/spaceship/spec/connect_api/token_spec.rb index 6c38d450ad3..9a2c10abab3 100644 --- a/spaceship/spec/connect_api/token_spec.rb +++ b/spaceship/spec/connect_api/token_spec.rb @@ -68,7 +68,7 @@ file.close expect do Spaceship::ConnectAPI::Token.from_json_file(file.path) - end.to raise_error("App Store Connect API key JSON is missing field(s): key_id, issuer_id, key") + end.to raise_error("App Store Connect API key JSON is missing field(s): key_id, key") end it 'raises error with missing key' do @@ -151,6 +151,22 @@ expect(token.duration).to eq(200) expect(token.in_house).to eq(true) end + + it "without issuer_id" do + expect(File).to receive(:binread).with('/path/to/file').and_return(private_key) + token = Spaceship::ConnectAPI::Token.create( + key_id: "key_id", + filepath: "/path/to/file", + duration: 200, + in_house: true + ) + + expect(token.key_id).to eq('key_id') + expect(token.issuer_id).to be_nil + expect(token.text).not_to(be_nil) + expect(token.duration).to eq(200) + expect(token.in_house).to eq(true) + end end describe 'with environment variables' do @@ -196,7 +212,7 @@ end context 'init' do - it 'generates proper token' do + it 'generates proper team token' do key = OpenSSL::PKey::EC.generate('prime256v1') token = Spaceship::ConnectAPI::Token.new(key_id: key_id, issuer_id: issuer_id, key: key) expect(token.key_id).to eq(key_id) @@ -205,7 +221,28 @@ payload, header = JWT.decode(token.text, key, true, { algorithm: 'ES256' }) expect(payload['iss']).to eq(issuer_id) - expect(payload['iat']).to eq(Time.now.to_i) + expect(payload['sub']).to be_nil + + expect(payload['iat']).to be < Time.now.to_i + expect(payload['aud']).to eq('appstoreconnect-v1') + expect(payload['exp']).to be > Time.now.to_i + + expect(header['kid']).to eq(key_id) + expect(header['typ']).to eq('JWT') + end + + it 'generates proper individual token' do + key = OpenSSL::PKey::EC.generate('prime256v1') + token = Spaceship::ConnectAPI::Token.new(key_id: key_id, key: key) + expect(token.key_id).to eq(key_id) + expect(token.issuer_id).to be_nil + + payload, header = JWT.decode(token.text, key, true, { algorithm: 'ES256' }) + + expect(payload['sub']).to eq('user') + expect(payload['iss']).to be_nil + + expect(payload['iat']).to be < Time.now.to_i expect(payload['aud']).to eq('appstoreconnect-v1') expect(payload['exp']).to be > Time.now.to_i diff --git a/spaceship/spec/connect_api/tunes/tunes_client_spec.rb b/spaceship/spec/connect_api/tunes/tunes_client_spec.rb index 73f1a78059e..c143e140fbf 100644 --- a/spaceship/spec/connect_api/tunes/tunes_client_spec.rb +++ b/spaceship/spec/connect_api/tunes/tunes_client_spec.rb @@ -45,9 +45,27 @@ def test_request_body(url, body) return req_mock end + describe "appAvailabilities" do + context 'get_app_availabilities' do + let(:path) { "v2/appAvailabilities" } + let(:app_id) { "123" } + + it 'succeeds' do + url = "#{path}/#{app_id}" + params = { + include: "territoryAvailabilities", + limit: { "territoryAvailabilities": 200 } + } + req_mock = test_request_params(url, params) + expect(client).to receive(:request).with(:get).and_yield(req_mock).and_return(req_mock) + client.get_app_availabilities(app_id: app_id, includes: "territoryAvailabilities", limit: { "territoryAvailabilities": 200 }) + end + end + end + describe "appStoreVersionReleaseRequests" do context 'post_app_store_version_release_request' do - let(:path) { "appStoreVersionReleaseRequests" } + let(:path) { "v1/appStoreVersionReleaseRequests" } let(:app_store_version_id) { "123" } let(:body) do { @@ -78,7 +96,7 @@ def test_request_body(url, body) describe "reviewSubmissions" do context 'get_review_submissions' do let(:app_id) { "123456789-app" } - let(:path) { "apps/#{app_id}/reviewSubmissions" } + let(:path) { "v1/apps/#{app_id}/reviewSubmissions" } it 'succeeds' do params = {} @@ -90,7 +108,7 @@ def test_request_body(url, body) context 'get_review_submission' do let(:review_submission_id) { "123456789" } - let(:path) { "reviewSubmissions/#{review_submission_id}" } + let(:path) { "v1/reviewSubmissions/#{review_submission_id}" } it 'succeeds' do params = {} @@ -103,7 +121,7 @@ def test_request_body(url, body) context 'post_review_submission' do let(:app_id) { "123456789-app" } let(:platform) { Spaceship::ConnectAPI::Platform::IOS } - let(:path) { "reviewSubmissions" } + let(:path) { "v1/reviewSubmissions" } let(:body) do { data: { @@ -135,7 +153,7 @@ def test_request_body(url, body) context 'patch_review_submission' do let(:review_submission_id) { "123456789" } let(:attributes) { { submitted: true } } - let(:path) { "reviewSubmissions/#{review_submission_id}" } + let(:path) { "v1/reviewSubmissions/#{review_submission_id}" } let(:body) do { data: { @@ -159,7 +177,7 @@ def test_request_body(url, body) describe "reviewSubmissionItems" do context 'get_review_submission_items' do let(:review_submission_id) { "123456789" } - let(:path) { "reviewSubmissions/#{review_submission_id}/items" } + let(:path) { "v1/reviewSubmissions/#{review_submission_id}/items" } it 'succeeds' do params = {} @@ -172,7 +190,7 @@ def test_request_body(url, body) context 'post_review_submission_item' do let(:review_submission_id) { "123456789" } let(:app_store_version_id) { "123456789-app-store-version" } - let(:path) { "reviewSubmissionItems" } + let(:path) { "v1/reviewSubmissionItems" } let(:body) do { data: { diff --git a/spaceship/spec/connect_api/tunes/tunes_stubbing.rb b/spaceship/spec/connect_api/tunes/tunes_stubbing.rb index 9e92fe9660a..8a060292a31 100644 --- a/spaceship/spec/connect_api/tunes/tunes_stubbing.rb +++ b/spaceship/spec/connect_api/tunes/tunes_stubbing.rb @@ -14,6 +14,21 @@ def stub_request(*args) WebMock::API.stub_request(*args) end + def stub_get_app_availabilities_ready_for_distribution + stub_request(:get, "https://appstoreconnect.apple.com/iris/v2/appAvailabilities/123456789?include=territoryAvailabilities&limit%5BterritoryAvailabilities%5D=200"). + to_return(status: 200, body: read_fixture_file('app_availabilities_ready_for_distribution.json'), headers: { 'Content-Type' => 'application/json' }) + end + + def stub_get_app_availabilities_removed_from_sale + stub_request(:get, "https://appstoreconnect.apple.com/iris/v2/appAvailabilities/123456789?include=territoryAvailabilities&limit%5BterritoryAvailabilities%5D=200"). + to_return(status: 200, body: read_fixture_file('app_availabilities_removed_app.json'), headers: { 'Content-Type' => 'application/json' }) + end + + def stub_get_app_infos + stub_request(:get, "https://appstoreconnect.apple.com/iris/v1/apps/123456789/appInfos"). + to_return(status: 200, body: read_fixture_file('app_infos.json'), headers: { 'Content-Type' => 'application/json' }) + end + def stub_app_store_version_release_request stub_request(:post, "https://appstoreconnect.apple.com/iris/v1/appStoreVersionReleaseRequests"). to_return(status: 200, body: read_fixture_file('app_store_version_release_request.json'), headers: { 'Content-Type' => 'application/json' }) diff --git a/spaceship/spec/connect_api/users/user_client_spec.rb b/spaceship/spec/connect_api/users/user_client_spec.rb index 032cd43803d..1cfbc24cb02 100644 --- a/spaceship/spec/connect_api/users/user_client_spec.rb +++ b/spaceship/spec/connect_api/users/user_client_spec.rb @@ -55,7 +55,7 @@ def test_request(url) describe "users" do context 'get_users' do - let(:path) { "users" } + let(:path) { "v1/users" } it 'succeeds' do params = {} @@ -70,7 +70,7 @@ def test_request(url) let(:all_apps_visible) { false } let(:provisioning_allowed) { true } let(:roles) { ["ADMIN"] } - let(:path) { "users/#{user_id}" } + let(:path) { "v1/users/#{user_id}" } let(:app_ids) { ["456", "789"] } let(:body) do { @@ -119,7 +119,7 @@ def test_request(url) context 'delete_user' do let(:user_id) { "123" } - let(:path) { "users/#{user_id}" } + let(:path) { "v1/users/#{user_id}" } it 'succeeds' do req_mock = test_request(path) @@ -130,7 +130,7 @@ def test_request(url) context 'post_user_visible_apps' do let(:user_id) { "123" } - let(:path) { "users/#{user_id}/relationships/visibleApps" } + let(:path) { "v1/users/#{user_id}/relationships/visibleApps" } let(:app_ids) { ["456", "789"] } let(:body) do { @@ -154,7 +154,7 @@ def test_request(url) context 'patch_user_visible_apps' do let(:user_id) { "123" } - let(:path) { "users/#{user_id}/relationships/visibleApps" } + let(:path) { "v1/users/#{user_id}/relationships/visibleApps" } let(:app_ids) { ["456", "789"] } let(:body) do { @@ -178,7 +178,7 @@ def test_request(url) context 'delete_user_visible_apps' do let(:user_id) { "123" } - let(:path) { "users/#{user_id}/relationships/visibleApps" } + let(:path) { "v1/users/#{user_id}/relationships/visibleApps" } let(:app_ids) { ["456", "789"] } let(:body) do { @@ -202,7 +202,7 @@ def test_request(url) context 'get_user_visible_apps' do let(:user_id) { "42" } - let(:path) { "users/#{user_id}/visibleApps" } + let(:path) { "v1/users/#{user_id}/visibleApps" } it 'succeeds' do params = {} @@ -215,7 +215,7 @@ def test_request(url) describe "user_invitations" do context 'get_user_invitations' do - let(:path) { "userInvitations" } + let(:path) { "v1/userInvitations" } it 'succeeds' do params = {} @@ -226,7 +226,7 @@ def test_request(url) end context 'post_user_invitation' do - let(:path) { "userInvitations" } + let(:path) { "v1/userInvitations" } let(:attributes) { { email: "test@example.com", @@ -276,7 +276,7 @@ def test_request(url) context 'delete_user_invitation' do let(:invitation_id) { "123" } - let(:path) { "userInvitations/#{invitation_id}" } + let(:path) { "v1/userInvitations/#{invitation_id}" } it 'succeeds' do req_mock = test_request(path) @@ -287,7 +287,7 @@ def test_request(url) context 'get_user_invitation_visible_apps' do let(:invitation_id) { "42" } - let(:path) { "userInvitations/#{invitation_id}/visibleApps" } + let(:path) { "v1/userInvitations/#{invitation_id}/visibleApps" } it 'succeeds' do params = {} diff --git a/spaceship/spec/mock_servers.rb b/spaceship/spec/mock_servers.rb index a2235c3eed0..37fb2e89398 100644 --- a/spaceship/spec/mock_servers.rb +++ b/spaceship/spec/mock_servers.rb @@ -5,9 +5,9 @@ config.include(WebMock::API) config.before do - stub_request(:any, %r(appstoreconnect.apple.com/testflight/v2)).to_rack(MockAPI::TestFlightServer) - stub_request(:any, %r(developer.apple.com/services-account/QH65B2/account/auth/key)).to_rack(MockAPI::DeveloperPortalServer) - stub_request(:any, %r(developer.apple.com/services-account/QH65B2/account/ios/identifiers/.*OMC(s){0,1}\.action)).to_rack(MockAPI::DeveloperPortalServer) + stub_request(:any, %r(appstoreconnect\.apple.com/testflight/v2)).to_rack(MockAPI::TestFlightServer) + stub_request(:any, %r(developer\.apple\.com/services-account/QH65B2/account/auth/key)).to_rack(MockAPI::DeveloperPortalServer) + stub_request(:any, %r(developer\.apple\.com/services-account/QH65B2/account/ios/identifiers/.*OMC(s){0,1}\.action)).to_rack(MockAPI::DeveloperPortalServer) end config.after do diff --git a/spaceship/spec/portal/portal_client_spec.rb b/spaceship/spec/portal/portal_client_spec.rb index e74ac64c9fb..8f4d84b6567 100644 --- a/spaceship/spec/portal/portal_client_spec.rb +++ b/spaceship/spec/portal/portal_client_spec.rb @@ -259,7 +259,7 @@ "deviceIds", "appId", "certificateIds") - expect(a_request(:post, /developerservices2.apple.com/)).to have_been_made + expect(a_request(:post, /developerservices2\.apple\.com/)).to have_been_made end end diff --git a/spaceship/spec/provisioning_profile_spec.rb b/spaceship/spec/provisioning_profile_spec.rb index 0ab1e1a4a69..6d78374f7c3 100644 --- a/spaceship/spec/provisioning_profile_spec.rb +++ b/spaceship/spec/provisioning_profile_spec.rb @@ -139,14 +139,22 @@ describe '#valid?' do it "Valid profile" do p = Spaceship::ProvisioningProfile.all.first + p.expires = Time.now.utc + 60 expect(p.valid?).to eq(true) end - it "Invalid profile" do + it "Invalid profile with expired status" do profile = Spaceship::ProvisioningProfile.all.first profile.status = 'Expired' expect(profile.valid?).to eq(false) end + + it "Invalid profile with active status but expired date" do + profile = Spaceship::ProvisioningProfile.all.first + profile.status = 'Active' + profile.expires = Time.now.utc + expect(profile.valid?).to eq(false) + end end describe '#factory' do diff --git a/spaceship/spec/stats_middleware_spec.rb b/spaceship/spec/stats_middleware_spec.rb index 48bcafb429a..cb65e3eab5b 100644 --- a/spaceship/spec/stats_middleware_spec.rb +++ b/spaceship/spec/stats_middleware_spec.rb @@ -64,8 +64,8 @@ expect(Spaceship::StatsMiddleware.service_stats.size).to eq(8) expect(find_count("api.appstoreconnect.apple.com")).to eq(2) - expect(find_count("appstoreconnect.apple.com/iris/v1/")).to eq(1) - expect(find_count("developer.apple.com/services-account/v1/")).to eq(1) + expect(find_count("appstoreconnect.apple.com/iris/")).to eq(1) + expect(find_count("developer.apple.com/services-account/")).to eq(1) expect(find_count("idmsa.apple.com")).to eq(1) expect(find_count("appstoreconnect.apple.com/olympus/v1/")).to eq(1) expect(find_count("appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/")).to eq(1) diff --git a/spaceship/spec/tunes/app_submission_spec.rb b/spaceship/spec/tunes/app_submission_spec.rb index 33606142f72..32abf38acef 100644 --- a/spaceship/spec/tunes/app_submission_spec.rb +++ b/spaceship/spec/tunes/app_submission_spec.rb @@ -21,23 +21,11 @@ submission = app.create_submission submission.content_rights_contains_third_party_content = true submission.content_rights_has_rights = true - submission.add_id_info_uses_idfa = false submission.complete! expect(submission.submitted_for_review).to eq(true) end - it "sets automatically the limitsTracking value for the usesIdfa" do - TunesStubbing.itc_stub_app_submissions - submission = app.create_submission - submission.content_rights_contains_third_party_content = true - submission.content_rights_has_rights = true - submission.add_id_info_uses_idfa = true - submission.complete! - - expect(submission.raw_data["adIdInfo"]["limitsTracking"]["value"]).to eq(true) - end - it "raises an error when submitting an app that has validation errors" do TunesStubbing.itc_stub_app_submissions_invalid @@ -51,7 +39,6 @@ submission = app.create_submission submission.content_rights_contains_third_party_content = true submission.content_rights_has_rights = true - submission.add_id_info_uses_idfa = false expect do submission.complete! diff --git a/spaceship/spec/tunes/fixtures/app_submission/complete_success.json b/spaceship/spec/tunes/fixtures/app_submission/complete_success.json index 7dbfbcad93a..5f07fb35e8c 100644 --- a/spaceship/spec/tunes/fixtures/app_submission/complete_success.json +++ b/spaceship/spec/tunes/fixtures/app_submission/complete_success.json @@ -27,12 +27,6 @@ "isEditable": false, "isRequired": false, "value": null - }, - "usesIdfa": { - "errorKeys": null, - "isEditable": false, - "isRequired": true, - "value": null } }, "contentRights": null, diff --git a/spaceship/spec/tunes/fixtures/app_submission/start_success.json b/spaceship/spec/tunes/fixtures/app_submission/start_success.json index b00336f6d2f..e3aea596695 100644 --- a/spaceship/spec/tunes/fixtures/app_submission/start_success.json +++ b/spaceship/spec/tunes/fixtures/app_submission/start_success.json @@ -27,12 +27,6 @@ "isEditable": false, "isRequired": false, "value": null - }, - "usesIdfa": { - "errorKeys": null, - "isEditable": false, - "isRequired": true, - "value": null } }, "contentRights": { diff --git a/spec_helper.rb b/spec_helper.rb index a4cabfae1d0..fc0fe79bc15 100644 --- a/spec_helper.rb +++ b/spec_helper.rb @@ -104,10 +104,13 @@ def before(*args) end # skip some more tests if run on on Windows - if FastlaneCore::Helper.windows? + if FastlaneCore::Helper.windows? || !system('which xar') config.define_derived_metadata(:requires_xar) do |meta| - meta[:skip] = "Skipped: Requires `xar` to be installed (which is not possible on Windows and no workaround has been implemented)" + meta[:skip] = "Skipped: Requires `xar` to be installed (which is not possible on Windows and some Linux distros and no workaround has been implemented)" end + end + + if FastlaneCore::Helper.windows? config.define_derived_metadata(:requires_pty) do |meta| meta[:skip] = "Skipped: Requires `pty` to be available (which is not possible on Windows and no workaround has been implemented)" end diff --git a/trainer/lib/trainer/test_parser.rb b/trainer/lib/trainer/test_parser.rb index c973043d290..cf5c4d79b95 100644 --- a/trainer/lib/trainer/test_parser.rb +++ b/trainer/lib/trainer/test_parser.rb @@ -197,12 +197,37 @@ def execute_cmd(cmd) return output end + # Hotfix: From Xcode 16 beta 3 'xcresulttool get --format json' has been deprecated; + # '--legacy' flag required to keep on using the command + def generate_cmd_parse_xcresult(path) + xcresulttool_cmd = %W( + xcrun + xcresulttool + get + --format + json + --path + #{path} + ) + + # e.g. DEVELOPER_DIR=/Applications/Xcode_16_beta_3.app + # xcresulttool version 23021, format version 3.53 (current) + match = `xcrun xcresulttool version`.match(/xcresulttool version (?[\d.]+)/) + version = match[:version] + xcresulttool_cmd << '--legacy' if Gem::Version.new(version) >= Gem::Version.new(23_021) + + xcresulttool_cmd.join(' ') + end + def parse_xcresult(path, output_remove_retry_attempts: false) require 'shellwords' path = Shellwords.escape(path) # Executes xcresulttool to get JSON format of the result bundle object - result_bundle_object_raw = execute_cmd("xcrun xcresulttool get --format json --path #{path}") + # Hotfix: From Xcode 16 beta 3 'xcresulttool get --format json' has been deprecated; '--legacy' flag required to keep on using the command + xcresulttool_cmd = generate_cmd_parse_xcresult(path) + + result_bundle_object_raw = execute_cmd(xcresulttool_cmd) result_bundle_object = JSON.parse(result_bundle_object_raw) # Parses JSON into ActionsInvocationRecord to find a list of all ids for ActionTestPlanRunSummaries @@ -215,7 +240,7 @@ def parse_xcresult(path, output_remove_retry_attempts: false) # Maps ids into ActionTestPlanRunSummaries by executing xcresulttool to get JSON # containing specific information for each test summary, summaries = ids.map do |id| - raw = execute_cmd("xcrun xcresulttool get --format json --path #{path} --id #{id}") + raw = execute_cmd("#{xcresulttool_cmd} --id #{id}") json = JSON.parse(raw) Trainer::XCResult::ActionTestPlanRunSummaries.new(json) end diff --git a/trainer/lib/trainer/xcresult.rb b/trainer/lib/trainer/xcresult.rb index 5e687a68295..934fabd97a2 100644 --- a/trainer/lib/trainer/xcresult.rb +++ b/trainer/lib/trainer/xcresult.rb @@ -173,6 +173,8 @@ def all_subtests end def find_failure(failures) + sanitizer = proc { |name| name.gsub(/\W/, "_") } + sanitized_identifier = sanitizer.call(self.identifier) if self.test_status == "Failure" # Tries to match failure on test case name # Example TestFailureIssueSummary: @@ -184,16 +186,10 @@ def find_failure(failures) # or identifier: "TestThisDude/testFailureJosh2" (when Objective-C) found_failure = failures.find do |failure| - # Clean test_case_name to match identifier format - # Sanitize for Swift by replacing "." for "/" - # Sanitize for Objective-C by removing "-", "[", "]", and replacing " " for ?/ - sanitized_test_case_name = failure.test_case_name - .tr(".", "/") - .tr("-", "") - .tr("[", "") - .tr("]", "") - .tr(" ", "/") - self.identifier == sanitized_test_case_name + # Sanitize both test case name and identifier in a consistent fashion, then replace all non-word + # chars with underscore, and compare them + sanitized_test_case_name = sanitizer.call(failure.test_case_name) + sanitized_identifier == sanitized_test_case_name end return found_failure else diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw== new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw== @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~7tsPhGURwp2J0Yr2xLwDkXd5H9rvQnnXoc484tVlZxE0zPq6ZxlGj-xtLgKa55IRf6U1u3DcC4nThWQCFyQWtw== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~7tsPhGURwp2J0Yr2xLwDkXd5H9rvQnnXoc484tVlZxE0zPq6ZxlGj-xtLgKa55IRf6U1u3DcC4nThWQCFyQWtw== new file mode 100644 index 00000000000..8f09fee2982 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~7tsPhGURwp2J0Yr2xLwDkXd5H9rvQnnXoc484tVlZxE0zPq6ZxlGj-xtLgKa55IRf6U1u3DcC4nThWQCFyQWtw== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~7wgiMormC4x4enRXpucJYkDFXsPwlIELWjwYYthhXqpoq1Jf3-pPX5RWUJNKLGrlSAnPrunazVEyYzzqPvwMdA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~7wgiMormC4x4enRXpucJYkDFXsPwlIELWjwYYthhXqpoq1Jf3-pPX5RWUJNKLGrlSAnPrunazVEyYzzqPvwMdA== new file mode 100644 index 00000000000..865199997ab Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~7wgiMormC4x4enRXpucJYkDFXsPwlIELWjwYYthhXqpoq1Jf3-pPX5RWUJNKLGrlSAnPrunazVEyYzzqPvwMdA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~96Gg-fMtc5VM-luuv1gvnNzTxyoeYQUgP4hhLCxgMi0c2wQPCGVwRe19qboVX1d3C9dnfZsZBpqv1B28isTPXA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~96Gg-fMtc5VM-luuv1gvnNzTxyoeYQUgP4hhLCxgMi0c2wQPCGVwRe19qboVX1d3C9dnfZsZBpqv1B28isTPXA== new file mode 100644 index 00000000000..9a0adba92e3 --- /dev/null +++ b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~96Gg-fMtc5VM-luuv1gvnNzTxyoeYQUgP4hhLCxgMi0c2wQPCGVwRe19qboVX1d3C9dnfZsZBpqv1B28isTPXA== @@ -0,0 +1 @@ +[{"name":"testmanagerd.log","type":1}] \ No newline at end of file diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~Dy9B07v7JcxXSDNAYT_cJWr3uvut6rtl1oSlYvgZ9vg5LIfHXOR_NANkgLj-Ts9MtIpbQDWei3dntyVgyXx-UQ== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~Dy9B07v7JcxXSDNAYT_cJWr3uvut6rtl1oSlYvgZ9vg5LIfHXOR_NANkgLj-Ts9MtIpbQDWei3dntyVgyXx-UQ== new file mode 100644 index 00000000000..18123aa8e48 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~Dy9B07v7JcxXSDNAYT_cJWr3uvut6rtl1oSlYvgZ9vg5LIfHXOR_NANkgLj-Ts9MtIpbQDWei3dntyVgyXx-UQ== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~HDNTGnB2FATq2C5mp44gsBoqg2tGbCBxHGz-OEOd7A4lBLUy-yn6JwIRXbr8KH1IzVATAPcw3Zn6xdy7ioiLVQ== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~HDNTGnB2FATq2C5mp44gsBoqg2tGbCBxHGz-OEOd7A4lBLUy-yn6JwIRXbr8KH1IzVATAPcw3Zn6xdy7ioiLVQ== new file mode 100644 index 00000000000..26c2f1cd325 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~HDNTGnB2FATq2C5mp44gsBoqg2tGbCBxHGz-OEOd7A4lBLUy-yn6JwIRXbr8KH1IzVATAPcw3Zn6xdy7ioiLVQ== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~HjnMbbAqP4qhK3WyCTi6tbfycJkNOUH6P4C1yK-qhxHc5VJRpQWtfEml_wNcFSW-_PemyWE9zkPyQmKvjrvmbw== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~HjnMbbAqP4qhK3WyCTi6tbfycJkNOUH6P4C1yK-qhxHc5VJRpQWtfEml_wNcFSW-_PemyWE9zkPyQmKvjrvmbw== new file mode 100644 index 00000000000..5167bf664c8 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~HjnMbbAqP4qhK3WyCTi6tbfycJkNOUH6P4C1yK-qhxHc5VJRpQWtfEml_wNcFSW-_PemyWE9zkPyQmKvjrvmbw== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~OgxMYTpxs86Jl2k5uEAKBK-WUl1KFLEpGLcYG1Zcp2XvjXeYVsGbuW4XAKDQL8BC9Uk4onT3R_BhKU1jNPdjpA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~OgxMYTpxs86Jl2k5uEAKBK-WUl1KFLEpGLcYG1Zcp2XvjXeYVsGbuW4XAKDQL8BC9Uk4onT3R_BhKU1jNPdjpA== new file mode 100644 index 00000000000..cb6053ef39d --- /dev/null +++ b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~OgxMYTpxs86Jl2k5uEAKBK-WUl1KFLEpGLcYG1Zcp2XvjXeYVsGbuW4XAKDQL8BC9Uk4onT3R_BhKU1jNPdjpA== @@ -0,0 +1 @@ +[{"name":"SpaceTestsTests-537E3596-221E-4A31-9389-071C5541B922","type":2},{"name":"scheduling.log","type":1}] \ No newline at end of file diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~SsRyiCHDfQl2m19SwdRuZY_vhc88s24Xb6jgs7SXLFpWpNV6sGPwMav-Pin-3_-rheMqlGi_0gxrcwMBrUii1w== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~SsRyiCHDfQl2m19SwdRuZY_vhc88s24Xb6jgs7SXLFpWpNV6sGPwMav-Pin-3_-rheMqlGi_0gxrcwMBrUii1w== new file mode 100644 index 00000000000..8ab81c5201f Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~SsRyiCHDfQl2m19SwdRuZY_vhc88s24Xb6jgs7SXLFpWpNV6sGPwMav-Pin-3_-rheMqlGi_0gxrcwMBrUii1w== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~TbaJ7YZ2rLSKefY8s8kIJwRlnJw1zzAReF82HpwBmwzs1QjJVvlbAoLeLsbgckVIWionKTnQB8CXjzNyJJ-sng== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~TbaJ7YZ2rLSKefY8s8kIJwRlnJw1zzAReF82HpwBmwzs1QjJVvlbAoLeLsbgckVIWionKTnQB8CXjzNyJJ-sng== new file mode 100644 index 00000000000..1a67c9fdc94 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~TbaJ7YZ2rLSKefY8s8kIJwRlnJw1zzAReF82HpwBmwzs1QjJVvlbAoLeLsbgckVIWionKTnQB8CXjzNyJJ-sng== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~YxHeunnkhdMlia-g23AMt_6XVxlW2CHTRsWKIgO1KJGUokPzoO6JZCzw9bzjIlUW0FEenbUTiQOnK9EVS388KA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~YxHeunnkhdMlia-g23AMt_6XVxlW2CHTRsWKIgO1KJGUokPzoO6JZCzw9bzjIlUW0FEenbUTiQOnK9EVS388KA== new file mode 100644 index 00000000000..0f14b9d37b2 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~YxHeunnkhdMlia-g23AMt_6XVxlW2CHTRsWKIgO1KJGUokPzoO6JZCzw9bzjIlUW0FEenbUTiQOnK9EVS388KA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~aC_M8PwRbiGMJPd_fGN8QYgz05tLco7dDKBMF8UT-8mGYSsYRts4ieQBgbpqDMzMH419zNEJKIcmqFfnljUytA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~aC_M8PwRbiGMJPd_fGN8QYgz05tLco7dDKBMF8UT-8mGYSsYRts4ieQBgbpqDMzMH419zNEJKIcmqFfnljUytA== new file mode 100644 index 00000000000..4b3189c1194 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~aC_M8PwRbiGMJPd_fGN8QYgz05tLco7dDKBMF8UT-8mGYSsYRts4ieQBgbpqDMzMH419zNEJKIcmqFfnljUytA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~bc0-HDG3G9sjnJO1MxacCownHuqI7v_dC_54GWNc4luhzWVZD3LFEVvG-mRQNwwIkPE66UKlH8zZJqGovjviAg== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~bc0-HDG3G9sjnJO1MxacCownHuqI7v_dC_54GWNc4luhzWVZD3LFEVvG-mRQNwwIkPE66UKlH8zZJqGovjviAg== new file mode 100644 index 00000000000..20da68d9771 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~bc0-HDG3G9sjnJO1MxacCownHuqI7v_dC_54GWNc4luhzWVZD3LFEVvG-mRQNwwIkPE66UKlH8zZJqGovjviAg== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~jmAosCXh5KF2Um2yHXkDITU_RJ5Wo2rpHVk7fxekThnBvAlAtUwp_0xEUsHc1cZohuP5ljks-vA49BtNCSiKcA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~jmAosCXh5KF2Um2yHXkDITU_RJ5Wo2rpHVk7fxekThnBvAlAtUwp_0xEUsHc1cZohuP5ljks-vA49BtNCSiKcA== new file mode 100644 index 00000000000..af712ebac31 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~jmAosCXh5KF2Um2yHXkDITU_RJ5Wo2rpHVk7fxekThnBvAlAtUwp_0xEUsHc1cZohuP5ljks-vA49BtNCSiKcA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~minvEuWnOqyIKSjgc-6Q7vITnmIGNrfhOFVCLd1WugzmLC6RsFTPwE6trHssX-o-zTstvD4Rl4wHcvRyRRRZxA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~minvEuWnOqyIKSjgc-6Q7vITnmIGNrfhOFVCLd1WugzmLC6RsFTPwE6trHssX-o-zTstvD4Rl4wHcvRyRRRZxA== new file mode 100644 index 00000000000..c6970c5513f Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/data.0~minvEuWnOqyIKSjgc-6Q7vITnmIGNrfhOFVCLd1WugzmLC6RsFTPwE6trHssX-o-zTstvD4Rl4wHcvRyRRRZxA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw== new file mode 100644 index 00000000000..f76dd238ade Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~4VqMqsI5lOfxRppnud6-VDWcNsU8J7VgFCJfW2dXPwOcAkvU-I8Um5yp9n0Zv6nr3VmcxYggaVMDFfR0U_vjKw== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~7tsPhGURwp2J0Yr2xLwDkXd5H9rvQnnXoc484tVlZxE0zPq6ZxlGj-xtLgKa55IRf6U1u3DcC4nThWQCFyQWtw== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~7tsPhGURwp2J0Yr2xLwDkXd5H9rvQnnXoc484tVlZxE0zPq6ZxlGj-xtLgKa55IRf6U1u3DcC4nThWQCFyQWtw== new file mode 100644 index 00000000000..f76dd238ade Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~7tsPhGURwp2J0Yr2xLwDkXd5H9rvQnnXoc484tVlZxE0zPq6ZxlGj-xtLgKa55IRf6U1u3DcC4nThWQCFyQWtw== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~7wgiMormC4x4enRXpucJYkDFXsPwlIELWjwYYthhXqpoq1Jf3-pPX5RWUJNKLGrlSAnPrunazVEyYzzqPvwMdA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~7wgiMormC4x4enRXpucJYkDFXsPwlIELWjwYYthhXqpoq1Jf3-pPX5RWUJNKLGrlSAnPrunazVEyYzzqPvwMdA== new file mode 100644 index 00000000000..f76dd238ade Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~7wgiMormC4x4enRXpucJYkDFXsPwlIELWjwYYthhXqpoq1Jf3-pPX5RWUJNKLGrlSAnPrunazVEyYzzqPvwMdA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~96Gg-fMtc5VM-luuv1gvnNzTxyoeYQUgP4hhLCxgMi0c2wQPCGVwRe19qboVX1d3C9dnfZsZBpqv1B28isTPXA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~96Gg-fMtc5VM-luuv1gvnNzTxyoeYQUgP4hhLCxgMi0c2wQPCGVwRe19qboVX1d3C9dnfZsZBpqv1B28isTPXA== new file mode 100644 index 00000000000..7ec020a410e Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~96Gg-fMtc5VM-luuv1gvnNzTxyoeYQUgP4hhLCxgMi0c2wQPCGVwRe19qboVX1d3C9dnfZsZBpqv1B28isTPXA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~Dy9B07v7JcxXSDNAYT_cJWr3uvut6rtl1oSlYvgZ9vg5LIfHXOR_NANkgLj-Ts9MtIpbQDWei3dntyVgyXx-UQ== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~Dy9B07v7JcxXSDNAYT_cJWr3uvut6rtl1oSlYvgZ9vg5LIfHXOR_NANkgLj-Ts9MtIpbQDWei3dntyVgyXx-UQ== new file mode 100644 index 00000000000..f76dd238ade Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~Dy9B07v7JcxXSDNAYT_cJWr3uvut6rtl1oSlYvgZ9vg5LIfHXOR_NANkgLj-Ts9MtIpbQDWei3dntyVgyXx-UQ== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~HDNTGnB2FATq2C5mp44gsBoqg2tGbCBxHGz-OEOd7A4lBLUy-yn6JwIRXbr8KH1IzVATAPcw3Zn6xdy7ioiLVQ== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~HDNTGnB2FATq2C5mp44gsBoqg2tGbCBxHGz-OEOd7A4lBLUy-yn6JwIRXbr8KH1IzVATAPcw3Zn6xdy7ioiLVQ== new file mode 100644 index 00000000000..9ad395b7225 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~HDNTGnB2FATq2C5mp44gsBoqg2tGbCBxHGz-OEOd7A4lBLUy-yn6JwIRXbr8KH1IzVATAPcw3Zn6xdy7ioiLVQ== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~HjnMbbAqP4qhK3WyCTi6tbfycJkNOUH6P4C1yK-qhxHc5VJRpQWtfEml_wNcFSW-_PemyWE9zkPyQmKvjrvmbw== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~HjnMbbAqP4qhK3WyCTi6tbfycJkNOUH6P4C1yK-qhxHc5VJRpQWtfEml_wNcFSW-_PemyWE9zkPyQmKvjrvmbw== new file mode 100644 index 00000000000..f76dd238ade Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~HjnMbbAqP4qhK3WyCTi6tbfycJkNOUH6P4C1yK-qhxHc5VJRpQWtfEml_wNcFSW-_PemyWE9zkPyQmKvjrvmbw== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~OgxMYTpxs86Jl2k5uEAKBK-WUl1KFLEpGLcYG1Zcp2XvjXeYVsGbuW4XAKDQL8BC9Uk4onT3R_BhKU1jNPdjpA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~OgxMYTpxs86Jl2k5uEAKBK-WUl1KFLEpGLcYG1Zcp2XvjXeYVsGbuW4XAKDQL8BC9Uk4onT3R_BhKU1jNPdjpA== new file mode 100644 index 00000000000..38775b3653f Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~OgxMYTpxs86Jl2k5uEAKBK-WUl1KFLEpGLcYG1Zcp2XvjXeYVsGbuW4XAKDQL8BC9Uk4onT3R_BhKU1jNPdjpA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~SsRyiCHDfQl2m19SwdRuZY_vhc88s24Xb6jgs7SXLFpWpNV6sGPwMav-Pin-3_-rheMqlGi_0gxrcwMBrUii1w== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~SsRyiCHDfQl2m19SwdRuZY_vhc88s24Xb6jgs7SXLFpWpNV6sGPwMav-Pin-3_-rheMqlGi_0gxrcwMBrUii1w== new file mode 100644 index 00000000000..2c7fed79a72 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~SsRyiCHDfQl2m19SwdRuZY_vhc88s24Xb6jgs7SXLFpWpNV6sGPwMav-Pin-3_-rheMqlGi_0gxrcwMBrUii1w== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~TbaJ7YZ2rLSKefY8s8kIJwRlnJw1zzAReF82HpwBmwzs1QjJVvlbAoLeLsbgckVIWionKTnQB8CXjzNyJJ-sng== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~TbaJ7YZ2rLSKefY8s8kIJwRlnJw1zzAReF82HpwBmwzs1QjJVvlbAoLeLsbgckVIWionKTnQB8CXjzNyJJ-sng== new file mode 100644 index 00000000000..f76dd238ade Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~TbaJ7YZ2rLSKefY8s8kIJwRlnJw1zzAReF82HpwBmwzs1QjJVvlbAoLeLsbgckVIWionKTnQB8CXjzNyJJ-sng== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~YxHeunnkhdMlia-g23AMt_6XVxlW2CHTRsWKIgO1KJGUokPzoO6JZCzw9bzjIlUW0FEenbUTiQOnK9EVS388KA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~YxHeunnkhdMlia-g23AMt_6XVxlW2CHTRsWKIgO1KJGUokPzoO6JZCzw9bzjIlUW0FEenbUTiQOnK9EVS388KA== new file mode 100644 index 00000000000..f76dd238ade Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~YxHeunnkhdMlia-g23AMt_6XVxlW2CHTRsWKIgO1KJGUokPzoO6JZCzw9bzjIlUW0FEenbUTiQOnK9EVS388KA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~aC_M8PwRbiGMJPd_fGN8QYgz05tLco7dDKBMF8UT-8mGYSsYRts4ieQBgbpqDMzMH419zNEJKIcmqFfnljUytA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~aC_M8PwRbiGMJPd_fGN8QYgz05tLco7dDKBMF8UT-8mGYSsYRts4ieQBgbpqDMzMH419zNEJKIcmqFfnljUytA== new file mode 100644 index 00000000000..91ff311a7ed Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~aC_M8PwRbiGMJPd_fGN8QYgz05tLco7dDKBMF8UT-8mGYSsYRts4ieQBgbpqDMzMH419zNEJKIcmqFfnljUytA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~bc0-HDG3G9sjnJO1MxacCownHuqI7v_dC_54GWNc4luhzWVZD3LFEVvG-mRQNwwIkPE66UKlH8zZJqGovjviAg== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~bc0-HDG3G9sjnJO1MxacCownHuqI7v_dC_54GWNc4luhzWVZD3LFEVvG-mRQNwwIkPE66UKlH8zZJqGovjviAg== new file mode 100644 index 00000000000..66f086ae1e4 Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~bc0-HDG3G9sjnJO1MxacCownHuqI7v_dC_54GWNc4luhzWVZD3LFEVvG-mRQNwwIkPE66UKlH8zZJqGovjviAg== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~jmAosCXh5KF2Um2yHXkDITU_RJ5Wo2rpHVk7fxekThnBvAlAtUwp_0xEUsHc1cZohuP5ljks-vA49BtNCSiKcA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~jmAosCXh5KF2Um2yHXkDITU_RJ5Wo2rpHVk7fxekThnBvAlAtUwp_0xEUsHc1cZohuP5ljks-vA49BtNCSiKcA== new file mode 100644 index 00000000000..f76dd238ade Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~jmAosCXh5KF2Um2yHXkDITU_RJ5Wo2rpHVk7fxekThnBvAlAtUwp_0xEUsHc1cZohuP5ljks-vA49BtNCSiKcA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~minvEuWnOqyIKSjgc-6Q7vITnmIGNrfhOFVCLd1WugzmLC6RsFTPwE6trHssX-o-zTstvD4Rl4wHcvRyRRRZxA== b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~minvEuWnOqyIKSjgc-6Q7vITnmIGNrfhOFVCLd1WugzmLC6RsFTPwE6trHssX-o-zTstvD4Rl4wHcvRyRRRZxA== new file mode 100644 index 00000000000..f76dd238ade Binary files /dev/null and b/trainer/spec/fixtures/Test.with_spaces.xcresult/Data/refs.0~minvEuWnOqyIKSjgc-6Q7vITnmIGNrfhOFVCLd1WugzmLC6RsFTPwE6trHssX-o-zTstvD4Rl4wHcvRyRRRZxA== differ diff --git a/trainer/spec/fixtures/Test.with_spaces.xcresult/Info.plist b/trainer/spec/fixtures/Test.with_spaces.xcresult/Info.plist new file mode 100644 index 00000000000..f5f345cbdf6 --- /dev/null +++ b/trainer/spec/fixtures/Test.with_spaces.xcresult/Info.plist @@ -0,0 +1,29 @@ + + + + + dateCreated + 2023-07-31T19:01:10Z + externalLocations + + rootId + + hash + 0~SsRyiCHDfQl2m19SwdRuZY_vhc88s24Xb6jgs7SXLFpWpNV6sGPwMav-Pin-3_-rheMqlGi_0gxrcwMBrUii1w== + + storage + + backend + fileBacked2 + compression + standard + + version + + major + 3 + minor + 39 + + + diff --git a/trainer/spec/test_parser_spec.rb b/trainer/spec/test_parser_spec.rb index 24ecf1388a6..e1b9e2f3810 100644 --- a/trainer/spec/test_parser_spec.rb +++ b/trainer/spec/test_parser_spec.rb @@ -26,6 +26,35 @@ end end + describe "#generate_cmd_parse_xcresult" do + let(:xcresult_sample_path) { "./trainer/spec/fixtures/Test.test_result.xcresult" } + let!(:subject) { Trainer::TestParser.new(xcresult_sample_path) } + let(:command) { subject.send(:generate_cmd_parse_xcresult, xcresult_sample_path) } + + before do + allow(File).to receive(:expand_path).with(xcresult_sample_path).and_return(xcresult_sample_path) + allow_any_instance_of(Trainer::TestParser).to receive(:`).with('xcrun xcresulttool version').and_return(version) + end + + context 'with >= Xcode 16 beta 3' do + let(:version) { 'xcresulttool version 23021, format version 3.53 (current)' } + let(:expected) { "xcrun xcresulttool get --format json --path #{xcresult_sample_path} --legacy" } + + it 'should pass `--legacy`', requires_xcode: true do + expect(command).to eq(expected) + end + end + + context 'with < Xcode 16 beta 3' do + let(:version) { 'xcresulttool version 22608.2, format version 3.49 (current)' } + let(:expected) { "xcrun xcresulttool get --format json --path #{xcresult_sample_path}" } + + it 'should not pass `--legacy`', requires_xcode: true do + expect(command).to eq(expected) + end + end + end + describe "Stores the data in a useful format" do describe "#tests_successful?" do it "returns false if tests failed" do @@ -207,6 +236,44 @@ expect(failure_messages).to eq(["XCTAssertTrue failed", "XCTAssertTrue failed"]) RSpec::Mocks.space.proxy_for(Trainer::XCResult::TestFailureIssueSummary).reset end + + it "works as expected with xcresult with spaces", requires_xcode: true do + tp = Trainer::TestParser.new("./trainer/spec/fixtures/Test.with_spaces.xcresult") + expect(tp.data).to eq([ + { + project_path: "SpaceTests.xcodeproj", + target_name: "SpaceTestsTests", + test_name: "SpaceTestsTests", + configuration_name: "Test Scheme Action", + duration: 0.21180307865142822, + tests: [ + { + identifier: "SpaceTestsSpec.a test with spaces, should always fail()", + name: "a test with spaces, should always fail()", + duration: 0.21180307865142822, + status: "Failure", + test_group: "SpaceTestsSpec", + guid: "", + failures: [ + { + failure_message: "expected to equal <1>, got <2>\n (/Users/mahmood.tahir/Developer/SpaceTests/SpaceTestsTests/TestSpec.swift#CharacterRangeLen=0&EndingLineNumber=15&StartingLineNumber=15)", + file_name: "", + line_number: 0, + message: "", + performance_failure: {} + } + ] + } + ], + number_of_tests: 1, + number_of_failures: 1, + number_of_skipped: 0, + number_of_tests_excluding_retries: 1, + number_of_failures_excluding_retries: 1, + number_of_retries: 0 + } + ]) + end end end end