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