diff --git a/.github/scripts/app_store_connect_release.rb b/.github/scripts/app_store_connect_release.rb index 6663e2ed..3261057e 100644 --- a/.github/scripts/app_store_connect_release.rb +++ b/.github/scripts/app_store_connect_release.rb @@ -269,10 +269,19 @@ def distribute_testflight(build) raise AppStoreConnectError, "TestFlight build cannot be distributed: #{state}" end end - @client.post("/v1/builds/#{build_id}/relationships/betaGroups", data: selected.map do |group| - { type: "betaGroups", id: group.fetch("id") } - end) - puts "Assigned #{@options[:platform]} build #{@options[:build_number]} to #{@options[:testflight_groups].join(', ')}" + selected.each do |group| + # Automatic internal groups already receive every build; Apple rejects + # manually assigning them. The group endpoint supports manual groups. + if group.dig("attributes", "isInternalGroup") && group.dig("attributes", "hasAccessToAllBuilds") + puts "#{group.dig('attributes', 'name')} already has automatic access to all builds" + next + end + + @client.post("/v1/betaGroups/#{group.fetch('id')}/relationships/builds", data: [ + { type: "builds", id: build_id } + ]) + end + puts "Distributed #{@options[:platform]} build #{@options[:build_number]} to #{@options[:testflight_groups].join(', ')}" end def find_or_create_version diff --git a/.github/scripts/test/app_store_connect_release_test.rb b/.github/scripts/test/app_store_connect_release_test.rb index 1b9da4b2..e4f38fd2 100644 --- a/.github/scripts/test/app_store_connect_release_test.rb +++ b/.github/scripts/test/app_store_connect_release_test.rb @@ -120,7 +120,7 @@ def patch(path, body) def beta_groups [ - { 'id' => 'internal', 'attributes' => { 'name' => 'Internal Testing', 'isInternalGroup' => true } }, + { 'id' => 'internal', 'attributes' => { 'name' => 'Internal Testing', 'isInternalGroup' => true, 'hasAccessToAllBuilds' => true } }, { 'id' => 'external', 'attributes' => { 'name' => 'External Testing', 'isInternalGroup' => false } } ] end @@ -131,11 +131,34 @@ def test_testflight_uses_only_existing_groups_and_beta_review release(client, process_only: false, testflight_groups: ['Internal Testing', 'External Testing']).run end assert client.mutations.any? { |_, path, _| path == '/v1/betaAppReviewSubmissions' } - assert_equal '/v1/builds/uploaded-build/relationships/betaGroups', client.mutations.last[1] - assert_equal %w[internal external], client.mutations.last[2][:data].map { |group| group[:id] } + assignments = client.mutations.select { |_, path, _| path.start_with?('/v1/betaGroups/') } + assert_equal [[:post, '/v1/betaGroups/external/relationships/builds', + { data: [{ type: 'builds', id: 'uploaded-build' }] }]], assignments + refute client.mutations.any? { |_, path, _| path.end_with?('/relationships/betaGroups') } refute client.mutations.any? { |_, path, _| path.include?('appStoreVersions') || path.include?('reviewSubmissions') } end + def test_automatic_internal_group_needs_no_assignment_or_beta_review + client = BetaClient.new([build], groups: beta_groups) + output, = capture_io do + release(client, process_only: false, testflight_groups: ['Internal Testing']).run + end + assert_match(/already has automatic access/, output) + refute client.mutations.any? { |_, path, _| path.include?('/relationships/') || path == '/v1/betaAppReviewSubmissions' } + end + + def test_manual_internal_group_uses_group_build_relationship_without_beta_review + groups = beta_groups + groups.first['attributes']['hasAccessToAllBuilds'] = false + client = BetaClient.new([build], groups: groups) + capture_io do + release(client, process_only: false, testflight_groups: ['Internal Testing']).run + end + assert_equal [:post, '/v1/betaGroups/internal/relationships/builds', + { data: [{ type: 'builds', id: 'uploaded-build' }] }], client.mutations.last + refute client.mutations.any? { |_, path, _| path == '/v1/betaAppReviewSubmissions' } + end + def test_missing_testflight_group_fails_without_changing_apple client = BetaClient.new([build], groups: beta_groups) assert_raises(AppStoreConnectError) do diff --git a/open_wearable/docs/apple-builds.md b/open_wearable/docs/apple-builds.md index ede387ce..cb2a7463 100644 --- a/open_wearable/docs/apple-builds.md +++ b/open_wearable/docs/apple-builds.md @@ -33,6 +33,10 @@ Xcode Cloud main-branch distribution settings, including automatic notification when Apple makes an external build available. It does not submit a public App Store release. +Internal groups with automatic access to all builds require no manual assignment. +Other internal and external groups receive the build through their group build +relationship; the build-to-groups endpoint rejects internal groups. + The workflow can also be run manually against a chosen ref. Manual runs default to upload-only validation; enable `distribute` to send the build to those groups.