diff --git a/Gemfile b/Gemfile
index 03b06c63fa2beedbfed656ba7ce444a8d9f0d5a6..3e58c6b6a64984fe971087badfdfdd180869ceaa 100644
--- a/Gemfile
+++ b/Gemfile
@@ -574,6 +574,7 @@ gem 'arr-pm', '~> 0.0.12'
# Apple plist parsing
gem 'CFPropertyList'
+gem 'app_store_connect'
# For phone verification
gem 'telesignenterprise', '~> 2.2'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 9c3173f19f40a8a156094e28e91aeeb6200520bd..7c46d84d4668ee58363c4077433f6907efaaaa3b 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -20,6 +20,7 @@
{"name":"akismet","version":"3.0.0","platform":"ruby","checksum":"74991b8e3d3257eeea996b47069abb8da2006c84a144255123e8dffd1c86b230"},
{"name":"android_key_attestation","version":"0.3.0","platform":"ruby","checksum":"467eb01a99d2bb48ef9cf24cc13712669d7056cba5a52d009554ff037560570b"},
{"name":"apollo_upload_server","version":"2.1.0","platform":"ruby","checksum":"e5f3c9dda0c2ca775d007072742b98d517dfd91a667111fedbcdc94dfabd904e"},
+{"name":"app_store_connect","version":"0.29.0","platform":"ruby","checksum":"01d7a923825a4221892099acb5a72f86f6ee7d8aa95815d3c459ba6816ea430f"},
{"name":"arr-pm","version":"0.0.12","platform":"ruby","checksum":"fdff482f75239239201f4d667d93424412639aad0b3b0ad4d827e7c637e0ad39"},
{"name":"asana","version":"0.10.13","platform":"ruby","checksum":"36d0d37f8dd6118a54580f1b80224875d7b6a9027598938e1722a508bfc2d7ac"},
{"name":"asciidoctor","version":"2.0.17","platform":"ruby","checksum":"ed5b5e399e8d64994cc16f0983f993d6e33990909a8415b6fc8b786cdeb00f3d"},
diff --git a/Gemfile.lock b/Gemfile.lock
index f12157122f95cc354b0f3d2e61d6e03d6c93c1ce..94811ebb2ee096e9030f23d675d970280ac4bcb3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -163,6 +163,9 @@ GEM
apollo_upload_server (2.1.0)
actionpack (>= 4.2)
graphql (>= 1.8)
+ app_store_connect (0.29.0)
+ activesupport (>= 6.0.0)
+ jwt (>= 1.4, <= 2.5.0)
arr-pm (0.0.12)
asana (0.10.13)
faraday (~> 1.0)
@@ -1578,6 +1581,7 @@ DEPENDENCIES
addressable (~> 2.8)
akismet (~> 3.0)
apollo_upload_server (~> 2.1.0)
+ app_store_connect
arr-pm (~> 0.0.12)
asana (~> 0.10.13)
asciidoctor (~> 2.0.17)
diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb
index 74d998503b7330fb4d6edd6d2465b6f4d3c5784b..1da612893ce8ef3562e2ef7d9b49fc25988bea41 100644
--- a/app/controllers/concerns/integrations/params.rb
+++ b/app/controllers/concerns/integrations/params.rb
@@ -5,6 +5,9 @@ module Params
extend ActiveSupport::Concern
ALLOWED_PARAMS_CE = [
+ :app_store_issuer_id,
+ :app_store_key_id,
+ :app_store_private_key,
:active,
:alert_events,
:api_key,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 34b5b637422fb48f15997524820e0d230be42486..d8a332ab12f787ede2361fa96073b425f9150143 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -68,6 +68,7 @@ class Build < Ci::Processable
delegate :service_specification, to: :runner_session, allow_nil: true
delegate :gitlab_deploy_token, to: :project
delegate :harbor_integration, to: :project
+ delegate :apple_app_store_integration, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
delegate :ensure_persistent_ref, to: :pipeline
delegate :enable_debug_trace!, to: :metadata
@@ -587,6 +588,7 @@ def persisted_variables
.append(key: 'CI_REPOSITORY_URL', value: repo_url.to_s, public: false)
.concat(deploy_token_variables)
.concat(harbor_variables)
+ .concat(apple_app_store_variables)
end
end
@@ -630,6 +632,13 @@ def harbor_variables
Gitlab::Ci::Variables::Collection.new(harbor_integration.ci_variables)
end
+ def apple_app_store_variables
+ return [] unless apple_app_store_integration.try(:activated?)
+ return [] unless pipeline.protected_ref?
+
+ Gitlab::Ci::Variables::Collection.new(apple_app_store_integration.ci_variables)
+ end
+
def features
{
trace_sections: true,
diff --git a/app/models/integration.rb b/app/models/integration.rb
index dfa3642cfe4f7fb448f8b0bb3b7c6050ca03deba..12616a7f39a70ecba2c9349712adcaef95ac1c05 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -278,10 +278,13 @@ def self.nonexistent_integration_types_for(scope)
# Returns a list of available integration names.
# Example: ["asana", ...]
# @deprecated
- def self.available_integration_names(include_project_specific: true, include_dev: true)
+ # @param [Boolean] include_feature_flagged used only in specs
+ # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/386731
+ def self.available_integration_names(include_project_specific: true, include_dev: true, include_feature_flagged: false)
names = integration_names
names += project_specific_integration_names if include_project_specific
names += dev_integration_names if include_dev
+ names << 'apple_app_store' if Feature.enabled?(:apple_app_store_integration) || include_feature_flagged
names.sort_by(&:downcase)
end
diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8418554293978f317f6fd7aa371f2c2965b200a9
--- /dev/null
+++ b/app/models/integrations/apple_app_store.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'app_store_connect'
+
+module Integrations
+ class AppleAppStore < Integration
+ ISSUER_ID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/.freeze
+ KEY_ID_REGEX = /\A(?=.*[A-Z])(?=.*[0-9])[A-Z0-9]+\z/.freeze
+
+ with_options if: :activated? do
+ validates :app_store_issuer_id, presence: true, format: { with: ISSUER_ID_REGEX }
+ validates :app_store_key_id, presence: true, format: { with: KEY_ID_REGEX }
+ validates :app_store_private_key, presence: true, certificate_key: true
+ end
+
+ field :app_store_issuer_id,
+ section: SECTION_TYPE_CONNECTION,
+ required: true,
+ title: -> { s_('AppleAppStore|The Apple App Store Connect Issuer ID.') }
+
+ field :app_store_key_id,
+ section: SECTION_TYPE_CONNECTION,
+ required: true,
+ title: -> { s_('AppleAppStore|The Apple App Store Connect Key ID.') },
+ is_secret: false
+
+ field :app_store_private_key,
+ section: SECTION_TYPE_CONNECTION,
+ required: true,
+ type: 'textarea',
+ title: -> { s_('AppleAppStore|The Apple App Store Connect Private Key.') },
+ is_secret: false
+
+ def title
+ 'Apple App Store Connect'
+ end
+
+ def description
+ s_('AppleAppStore|Use GitLab to build and release an app in the Apple App Store.')
+ end
+
+ def help
+ variable_list = [
+ 'APP_STORE_CONNECT_API_KEY_ISSUER_ID',
+ 'APP_STORE_CONNECT_API_KEY_KEY_ID',
+ 'APP_STORE_CONNECT_API_KEY_KEY'
+ ]
+
+ # rubocop:disable Layout/LineLength
+ texts = [
+ s_("Use the Apple App Store Connect integration to easily connect to the Apple App Store with Fastlane in CI/CD pipelines."),
+ s_("After the Apple App Store Connect integration is activated, the following protected variables will be created for CI/CD use."),
+ variable_list.join('
'),
+ s_(format("To get started, see the integration documentation for instructions on how to generate App Store Connect credentials, and how to use this integration.", url: "https://docs.gitlab.com/ee/integration/apple_app_store.html")).html_safe
+ ]
+ # rubocop:enable Layout/LineLength
+
+ texts.join('
'.html_safe)
+ end
+
+ def self.to_param
+ 'apple_app_store'
+ end
+
+ def self.supported_events
+ []
+ end
+
+ def sections
+ [
+ {
+ type: SECTION_TYPE_CONNECTION,
+ title: s_('Integrations|Integration details'),
+ description: help
+ }
+ ]
+ end
+
+ def test(*_args)
+ response = client.apps
+ if response.has_key?(:errors)
+ { success: false, message: response[:errors].first[:title] }
+ else
+ { success: true }
+ end
+ end
+
+ def ci_variables
+ return [] unless activated?
+
+ [
+ { key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', value: app_store_issuer_id, masked: true, public: false },
+ { key: 'APP_STORE_CONNECT_API_KEY_KEY', value: Base64.encode64(app_store_private_key), masked: true,
+ public: false },
+ { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: app_store_key_id, masked: true, public: false }
+ ]
+ end
+
+ private
+
+ def client
+ config = {
+ issuer_id: app_store_issuer_id,
+ key_id: app_store_key_id,
+ private_key: app_store_private_key
+ }
+
+ AppStoreConnect::Client.new(config)
+ end
+ end
+end
diff --git a/app/models/integrations/field.rb b/app/models/integrations/field.rb
index 53c8f5f623eb6e9b3355bc72b91fb6c3fba7e061..329c046075f6eeec25602e0da01ff8ac40cfb72b 100644
--- a/app/models/integrations/field.rb
+++ b/app/models/integrations/field.rb
@@ -4,7 +4,7 @@ module Integrations
class Field
SECRET_NAME = %r/token|key|password|passphrase|secret/.freeze
- BOOLEAN_ATTRIBUTES = %i[required api_only exposes_secrets].freeze
+ BOOLEAN_ATTRIBUTES = %i[required api_only is_secret exposes_secrets].freeze
ATTRIBUTES = %i[
section type placeholder choices value checkbox_label
@@ -17,12 +17,13 @@ class Field
attr_reader :name, :integration_class
- def initialize(name:, integration_class:, type: 'text', api_only: false, **attributes)
+ def initialize(name:, integration_class:, type: 'text', is_secret: true, api_only: false, **attributes)
@name = name.to_s.freeze
@integration_class = integration_class
- attributes[:type] = SECRET_NAME.match?(@name) ? 'password' : type
+ attributes[:type] = SECRET_NAME.match?(@name) && is_secret ? 'password' : type
attributes[:api_only] = api_only
+ attributes[:is_secret] = is_secret
@attributes = attributes.freeze
invalid_attributes = attributes.keys - ATTRIBUTES
diff --git a/app/models/project.rb b/app/models/project.rb
index 91527f9f76d97601e860f83b4bec2f5799a1b3bf..9c56e152fb6f3a6bcfb441904af299bd07dd9a88 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -170,6 +170,7 @@ def self.integration_association_name(name)
end
# Project integrations
+ has_one :apple_app_store_integration, class_name: 'Integrations::AppleAppStore'
has_one :asana_integration, class_name: 'Integrations::Asana'
has_one :assembla_integration, class_name: 'Integrations::Assembla'
has_one :bamboo_integration, class_name: 'Integrations::Bamboo'
diff --git a/config/feature_flags/development/apple_app_store_integration.yml b/config/feature_flags/development/apple_app_store_integration.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ec55f1ef9326a8f845c336fa5a09159eefc31724
--- /dev/null
+++ b/config/feature_flags/development/apple_app_store_integration.yml
@@ -0,0 +1,8 @@
+---
+name: apple_app_store_integration
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/385335
+milestone: '15.8'
+type: development
+group: group::incubation
+default_enabled: false
diff --git a/config/metrics/counts_all/20221209212603_projects_inheriting_apple_app_store_active.yml b/config/metrics/counts_all/20221209212603_projects_inheriting_apple_app_store_active.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5e00246a15ce55a72e46c0413a8e0981e2b4ef27
--- /dev/null
+++ b/config/metrics/counts_all/20221209212603_projects_inheriting_apple_app_store_active.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.projects_inheriting_apple_app_store_active
+description: Count of active projects inheriting integrations for Apple App Store
+product_section: dev
+product_stage: manage
+product_group: integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "15.8"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20221209213642_groups_apple_app_store_active.yml b/config/metrics/counts_all/20221209213642_groups_apple_app_store_active.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9099752c62c286a64f0bb914af84f322a6f3211f
--- /dev/null
+++ b/config/metrics/counts_all/20221209213642_groups_apple_app_store_active.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.groups_apple_app_store_active
+description: Count of active groups inheriting integrations for Apple App Store
+product_section: dev
+product_stage: manage
+product_group: integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "15.8"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20221209214020_projects_apple_app_store_active.yml b/config/metrics/counts_all/20221209214020_projects_apple_app_store_active.yml
new file mode 100644
index 0000000000000000000000000000000000000000..92e9acbcca0051075ff2a06ef66d29c0319ec178
--- /dev/null
+++ b/config/metrics/counts_all/20221209214020_projects_apple_app_store_active.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.projects_apple_app_store_active
+description: Count of projects with active integrations for Apple App Store
+product_section: dev
+product_stage: manage
+product_group: integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "15.8"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20221209233053_groups_inheriting_apple_app_store_active.yml b/config/metrics/counts_all/20221209233053_groups_inheriting_apple_app_store_active.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f7835a4e072a4c0f127c8df63d36bebace63b3f6
--- /dev/null
+++ b/config/metrics/counts_all/20221209233053_groups_inheriting_apple_app_store_active.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.groups_inheriting_apple_app_store_active
+description: Count of active groups inheriting integrations for Apple App Store
+product_section: dev
+product_stage: manage
+product_group: integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "15.8"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20221209233201_instances_apple_app_store_active.yml b/config/metrics/counts_all/20221209233201_instances_apple_app_store_active.yml
new file mode 100644
index 0000000000000000000000000000000000000000..436f869cf0d2d8688e895411394c8004550cacdb
--- /dev/null
+++ b/config/metrics/counts_all/20221209233201_instances_apple_app_store_active.yml
@@ -0,0 +1,22 @@
+---
+key_path: counts.instances_apple_app_store_active
+description: Count of instances with active integrations for Apple App Store
+product_section: dev
+product_stage: manage
+product_group: integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "15.8"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888
+time_frame: all
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 682a974fb4dd350dd6d5b3af5ec2cd4e8653992b..e23eb83707127968484137c2932d583b774143c5 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -22536,6 +22536,7 @@ State of a Sentry error.
| Value | Description |
| ----- | ----------- |
+| `APPLE_APP_STORE_SERVICE` | AppleAppStoreService type. |
| `ASANA_SERVICE` | AsanaService type. |
| `ASSEMBLA_SERVICE` | AssemblaService type. |
| `BAMBOO_SERVICE` | BambooService type. |
diff --git a/doc/api/integrations.md b/doc/api/integrations.md
index f6ad095aad6f47c879f03648f44141bb19683787..24e0f189aad67da16099d410bde5fe3ece969d70 100644
--- a/doc/api/integrations.md
+++ b/doc/api/integrations.md
@@ -72,6 +72,44 @@ Example response:
]
```
+## Apple App Store
+
+Use GitLab to build and release an app in the Apple App Store.
+
+See also the [Apple App Store integration documentation](../user/project/integrations/apple_app_store.md).
+
+### Create/Edit Apple App Store integration
+
+Set Apple App Store integration for a project.
+
+```plaintext
+PUT /projects/:id/integrations/apple_app_store
+```
+
+Parameters:
+
+| Parameter | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `app_store_issuer_id` | string | true | The Apple App Store Connect Issuer ID. |
+| `app_store_key_id` | string | true | The Apple App Store Connect Key ID. |
+| `app_store_private_key` | string | true | The Apple App Store Connect Private Key. |
+
+### Disable Apple App Store integration
+
+Disable the Apple App Store integration for a project. Integration settings are preserved.
+
+```plaintext
+DELETE /projects/:id/integrations/apple_app_store
+```
+
+### Get Apple App Store integration settings
+
+Get Apple App Store integration settings for a project.
+
+```plaintext
+GET /projects/:id/integrations/apple_app_store
+```
+
## Asana
Add commit messages as comments to Asana tasks.
diff --git a/doc/user/project/integrations/apple_app_store.md b/doc/user/project/integrations/apple_app_store.md
new file mode 100644
index 0000000000000000000000000000000000000000..f381e71812c1e710757599ee56f5f4e6a88ea56a
--- /dev/null
+++ b/doc/user/project/integrations/apple_app_store.md
@@ -0,0 +1,57 @@
+---
+stage: Verify
+group: Integrations
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
+---
+
+# Apple App Store integration **(FREE)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888) in GitLab 15.8.
+
+The Apple App Store integration makes it easy to configure your CI/CD pipelines to connect to [App Store Connect](https://appstoreconnect.apple.com) to build and release apps for iOS, iPadOS, macOS, tvOS, and watchOS.
+
+The integration is designed to be able to work out of the box with [fastlane](http://fastlane.tools/), but can be used with other build tools as well.
+
+## Prerequisites
+
+An Apple ID enrolled in the [Apple Developer Program](https://developer.apple.com/programs/enroll/) is required to enable this integration.
+
+## Configure GitLab
+
+GitLab supports enabling the Apple App Store integration at the group or project level. Complete these steps in GitLab:
+
+1. In the Apple App Store Connect portal, generate a new private key for your project by following [these instructions](https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api).
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Settings > Integrations**.
+1. Select **Apple App Store**.
+1. Turn on the **Active** toggle under **Enable Integration**.
+1. Provide the Apple App Store Connect configuration information:
+ - **Issuer ID**: The Apple App Store Connect Issuer ID can be found in the *Keys* section under *Users and Access* the Apple App Store Connect portal.
+ - **Key ID**: The Key ID of the new private key that was just generated.
+ - **Private Key**: The Private Key that was just generated. Note: you are only be able to download this key one time.
+
+1. Select **Save changes**.
+
+After the Apple App Store integration is activated:
+
+- The global variables `$APP_STORE_CONNECT_API_KEY_ISSUER_ID`, `$APP_STORE_CONNECT_API_KEY_KEY_ID`, and `$APP_STORE_CONNECT_API_KEY_KEY` are created for CI/CD use.
+- `$APP_STORE_CONNECT_API_KEY_KEY` contains the Base64 encoded Private Key.
+- The project-level integration settings override the group-level integration settings.
+
+## Security considerations
+
+### CI/CD variable security
+
+Malicious code pushed to your `.gitlab-ci.yml` file could compromise your variables, including
+`$APP_STORE_CONNECT_API_KEY_KEY`, and send them to a third-party server. For more details, see
+[CI/CD variable security](../../../ci/variables/index.md#cicd-variable-security).
+
+## fastlane Example
+
+Because this integration works out of the box with fastlane adding the code below to an app's `fastlane/Fastfile` activates the integration, and create the connection for any interactions with the Apple App Store uploading a Test Flight or public App Store release.
+
+```ruby
+app_store_connect_api_key(
+ is_key_content_base64: true
+)
+```
diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb
index 543449c034982714ebc7fdf55a8cfabb7ad17d42..e2549c4bffbecfa3accf6c62f31fd528a5252b2d 100644
--- a/lib/api/helpers/integrations_helpers.rb
+++ b/lib/api/helpers/integrations_helpers.rb
@@ -161,6 +161,26 @@ def self.chat_notification_events
def self.integrations
{
+ 'apple-app-store' => [
+ {
+ required: true,
+ name: :app_store_issuer_id,
+ type: String,
+ desc: 'The Apple App Store Connect Issuer ID'
+ },
+ {
+ required: true,
+ name: :app_store_key_id,
+ type: String,
+ desc: 'The Apple App Store Connect Key ID'
+ },
+ {
+ required: true,
+ name: :app_store_private_key,
+ type: String,
+ desc: 'The Apple App Store Connect Private Key'
+ }
+ ],
'asana' => [
{
required: true,
@@ -871,6 +891,7 @@ def self.integrations
def self.integration_classes
[
+ ::Integrations::AppleAppStore,
::Integrations::Asana,
::Integrations::Assembla,
::Integrations::Bamboo,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a0cfd270916892d38805ef48d6875f2578d3265e..c65af7461d74f388d082c8541abbcd32d0f63c52 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3601,6 +3601,9 @@ msgstr ""
msgid "After it expires, you can't use merge approvals, epics, or many security features."
msgstr ""
+msgid "After the Apple App Store Connect integration is activated, the following protected variables will be created for CI/CD use."
+msgstr ""
+
msgid "After the export is complete, download the data file from a notification email or from this page. You can then import the data file from the %{strong_text_start}Create new group%{strong_text_end} page of another GitLab instance."
msgstr ""
@@ -4710,6 +4713,18 @@ msgstr ""
msgid "Append the comment with %{tableflip}"
msgstr ""
+msgid "AppleAppStore|The Apple App Store Connect Issuer ID."
+msgstr ""
+
+msgid "AppleAppStore|The Apple App Store Connect Key ID."
+msgstr ""
+
+msgid "AppleAppStore|The Apple App Store Connect Private Key."
+msgstr ""
+
+msgid "AppleAppStore|Use GitLab to build and release an app in the Apple App Store."
+msgstr ""
+
msgid "Application"
msgstr ""
@@ -22239,6 +22254,9 @@ msgstr ""
msgid "Integrations|Instance-level integration management"
msgstr ""
+msgid "Integrations|Integration details"
+msgstr ""
+
msgid "Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira."
msgstr ""
@@ -44682,6 +44700,9 @@ msgstr ""
msgid "Use the %{strongStart}Test%{strongEnd} option above to create an event."
msgstr ""
+msgid "Use the Apple App Store Connect integration to easily connect to the Apple App Store with Fastlane in CI/CD pipelines."
+msgstr ""
+
msgid "Use the link below to confirm your email address (%{email})"
msgstr ""
diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb
index ebbf1b560e53c925295a76f8a33a1e1cf1321461..7740b2da911fc0950ac3256ca6e06c773cbbd6a4 100644
--- a/spec/factories/integrations.rb
+++ b/spec/factories/integrations.rb
@@ -254,6 +254,16 @@
password { 'harborpassword' }
end
+ factory :apple_app_store_integration, class: 'Integrations::AppleAppStore' do
+ project
+ active { true }
+ type { 'Integrations::AppleAppStore' }
+
+ app_store_issuer_id { 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' }
+ app_store_key_id { 'ABC1' }
+ app_store_private_key { File.read('spec/fixtures/ssl_key.pem') }
+ end
+
# this is for testing storing values inside properties, which is deprecated and will be removed in
# https://gitlab.com/gitlab-org/gitlab/issues/29404
trait :without_properties_callback do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index b34399d20f140f43328a15cb160388741d0b933f..8175b60c76724beda4f6db235f1e6d43692a731a 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -423,6 +423,7 @@ project:
- deployment_hooks_integrations
- alert_hooks_integrations
- vulnerability_hooks_integrations
+- apple_app_store_integration
- campfire_integration
- confluence_integration
- datadog_integration
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 39ea3fafec38e1c370e87d9e6c3e788fe9463877..36630488584c06478deccb4a5c4f4df66b2a39d8 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3532,6 +3532,52 @@
end
end
+ context 'for the apple_app_store integration' do
+ let_it_be(:apple_app_store_integration) { create(:apple_app_store_integration) }
+
+ let(:apple_app_store_variables) do
+ [
+ { key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', value: apple_app_store_integration.app_store_issuer_id, masked: true, public: false },
+ { key: 'APP_STORE_CONNECT_API_KEY_KEY', value: Base64.encode64(apple_app_store_integration.app_store_private_key), masked: true, public: false },
+ { key: 'APP_STORE_CONNECT_API_KEY_KEY_ID', value: apple_app_store_integration.app_store_key_id, masked: true, public: false }
+ ]
+ end
+
+ context 'when the apple_app_store exists' do
+ context 'when a build is protected' do
+ before do
+ allow(build.pipeline).to receive(:protected_ref?).and_return(true)
+ build.project.update!(apple_app_store_integration: apple_app_store_integration)
+ end
+
+ it 'includes apple_app_store variables' do
+ is_expected.to include(*apple_app_store_variables)
+ end
+ end
+
+ context 'when a build is not protected' do
+ before do
+ allow(build.pipeline).to receive(:protected_ref?).and_return(false)
+ build.project.update!(apple_app_store_integration: apple_app_store_integration)
+ end
+
+ it 'does not include the apple_app_store variables' do
+ expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_ISSUER_ID' }).to be_nil
+ expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY' }).to be_nil
+ expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY_ID' }).to be_nil
+ end
+ end
+ end
+
+ context 'when the apple_app_store integration does not exist' do
+ it 'does not include apple_app_store variables' do
+ expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_ISSUER_ID' }).to be_nil
+ expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY' }).to be_nil
+ expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY_ID' }).to be_nil
+ end
+ end
+ end
+
context 'when build has dependency which has dotenv variable' do
let!(:prepare) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: [prepare.name] }) }
diff --git a/spec/models/integrations/apple_app_store_spec.rb b/spec/models/integrations/apple_app_store_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dde26e383c73c98f82b5002a97db402ab51f89a0
--- /dev/null
+++ b/spec/models/integrations/apple_app_store_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
+ describe 'Validations' do
+ context 'when active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of :app_store_issuer_id }
+ it { is_expected.to validate_presence_of :app_store_key_id }
+ it { is_expected.to validate_presence_of :app_store_private_key }
+ it { is_expected.to allow_value('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee').for(:app_store_issuer_id) }
+ it { is_expected.not_to allow_value('abcde').for(:app_store_issuer_id) }
+ it { is_expected.to allow_value(File.read('spec/fixtures/ssl_key.pem')).for(:app_store_private_key) }
+ it { is_expected.not_to allow_value("foo").for(:app_store_private_key) }
+ it { is_expected.to allow_value('ABCD1EF12G').for(:app_store_key_id) }
+ it { is_expected.not_to allow_value('ABC').for(:app_store_key_id) }
+ it { is_expected.not_to allow_value('abc1').for(:app_store_key_id) }
+ it { is_expected.not_to allow_value('-A0-').for(:app_store_key_id) }
+ end
+ end
+
+ context 'when integration is enabled' do
+ let(:apple_app_store_integration) { build(:apple_app_store_integration) }
+
+ describe '#fields' do
+ it 'returns custom fields' do
+ expect(apple_app_store_integration.fields.pluck(:name)).to eq(%w[app_store_issuer_id app_store_key_id
+ app_store_private_key])
+ end
+ end
+
+ describe '#test' do
+ it 'returns true for a successful request' do
+ allow(AppStoreConnect::Client).to receive_message_chain(:new, :apps).and_return({})
+ expect(apple_app_store_integration.test[:success]).to be true
+ end
+
+ it 'returns false for an invalid request' do
+ allow(AppStoreConnect::Client).to receive_message_chain(:new,
+:apps).and_return({ errors: [title: "error title"] })
+ expect(apple_app_store_integration.test[:success]).to be false
+ end
+ end
+
+ describe '#help' do
+ it 'renders prompt information' do
+ expect(apple_app_store_integration.help).not_to be_empty
+ end
+ end
+
+ describe '.to_param' do
+ it 'returns the name of the integration' do
+ expect(described_class.to_param).to eq('apple_app_store')
+ end
+ end
+
+ describe '#ci_variables' do
+ let(:apple_app_store_integration) { build_stubbed(:apple_app_store_integration) }
+
+ it 'returns vars when the integration is activated' do
+ ci_vars = [
+ {
+ key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID',
+ value: apple_app_store_integration.app_store_issuer_id,
+ masked: true,
+ public: false
+ },
+ {
+ key: 'APP_STORE_CONNECT_API_KEY_KEY',
+ value: Base64.encode64(apple_app_store_integration.app_store_private_key),
+ masked: true,
+ public: false
+ },
+ {
+ key: 'APP_STORE_CONNECT_API_KEY_KEY_ID',
+ value: apple_app_store_integration.app_store_key_id,
+ masked: true,
+ public: false
+ }
+ ]
+
+ expect(apple_app_store_integration.ci_variables).to match_array(ci_vars)
+ end
+
+ it 'returns an empty array when the integration is disabled' do
+ apple_app_store_integration = build_stubbed(:apple_app_store_integration, active: false)
+ expect(apple_app_store_integration.ci_variables).to match_array([])
+ end
+ end
+ end
+
+ context 'when integration is disabled' do
+ let(:apple_app_store_integration) { build_stubbed(:apple_app_store_integration, active: false) }
+
+ describe '#ci_variables' do
+ it 'returns an empty array' do
+ expect(apple_app_store_integration.ci_variables).to match_array([])
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/every_integration_spec.rb b/spec/models/integrations/every_integration_spec.rb
index 33e89b3dabcea61481f3fef6da5259fd65a4efea..8666ef512fcb02802407f192ff8cfd9d3e88472b 100644
--- a/spec/models/integrations/every_integration_spec.rb
+++ b/spec/models/integrations/every_integration_spec.rb
@@ -11,9 +11,9 @@
let(:integration) { integration_class.new }
context 'secret fields', :aggregate_failures do
- it "uses type: 'password' for all secret fields" do
+ it "uses type: 'password' for all secret fields, except when bypassed" do
integration.fields.each do |field|
- next unless Integrations::Field::SECRET_NAME.match?(field[:name])
+ next unless Integrations::Field::SECRET_NAME.match?(field[:name]) && field[:is_secret]
expect(field[:type]).to eq('password'),
"Field '#{field[:name]}' should use type 'password'"
diff --git a/spec/models/integrations/field_spec.rb b/spec/models/integrations/field_spec.rb
index 642fb1fbf7ffaf876e6915c05f921f56f874a189..c30f9ef0d7b79744ac6ad33c7a9767cd77633b57 100644
--- a/spec/models/integrations/field_spec.rb
+++ b/spec/models/integrations/field_spec.rb
@@ -83,6 +83,8 @@ def self.default_placeholder
be false
when :type
eq 'text'
+ when :is_secret
+ eq true
else
be_nil
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 04edb755b58f8168a54e448aeb1116b6c728b99c..3ce26906c27f3ac64874d0230e0c8eac43147b5b 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -47,6 +47,7 @@
it { is_expected.to have_one(:webex_teams_integration) }
it { is_expected.to have_one(:packagist_integration) }
it { is_expected.to have_one(:pushover_integration) }
+ it { is_expected.to have_one(:apple_app_store_integration) }
it { is_expected.to have_one(:asana_integration) }
it { is_expected.to have_many(:boards) }
it { is_expected.to have_one(:campfire_integration) }
diff --git a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
index ca2fe8a6c54a20575fa1b44a958f8640ef4bbb38..005046166d87df2a943eb6489a666938bb32130b 100644
--- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
+++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-Integration.available_integration_names.each do |integration|
+Integration.available_integration_names(include_feature_flagged: true).each do |integration|
RSpec.shared_context integration do
include JiraIntegrationHelpers if integration == 'jira'
@@ -40,7 +40,7 @@
let(:integration_attrs) do
integration_attrs_list.inject({}) do |hash, k|
- if k =~ /^(token*|.*_token|.*_key)/
+ if k =~ /^(token*|.*_token|.*_key)/ && k =~ /^[^app_store]/
hash.merge!(k => 'secrettoken')
elsif integration == 'confluence' && k == :confluence_url
hash.merge!(k => 'https://example.atlassian.net/wiki')
@@ -68,6 +68,12 @@
hash.merge!(k => "match_any")
elsif integration == 'campfire' && k == :room
hash.merge!(k => '1234')
+ elsif integration == 'apple_app_store' && k == :app_store_issuer_id
+ hash.merge!(k => 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee')
+ elsif integration == 'apple_app_store' && k == :app_store_private_key
+ hash.merge!(k => File.read('spec/fixtures/ssl_key.pem'))
+ elsif integration == 'apple_app_store' && k == :app_store_key_id
+ hash.merge!(k => 'ABC1')
else
hash.merge!(k => "someword")
end