From 8e384130830dbbeb321bed3c5524f95d6f7565d9 Mon Sep 17 00:00:00 2001 From: Darby Frey Date: Wed, 23 Nov 2022 10:31:36 -0600 Subject: [PATCH 1/5] Adding Apple App Store Integration Apple App Store integration added Changelog: changed MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888 --- Gemfile | 1 + Gemfile.checksum | 1 + Gemfile.lock | 4 + .../concerns/integrations/params.rb | 3 + app/models/ci/build.rb | 9 ++ app/models/integration.rb | 1 + app/models/integrations/apple_app_store.rb | 111 ++++++++++++++++++ app/models/integrations/field.rb | 7 +- app/models/project.rb | 1 + .../apple_app_store_integration.yml | 8 ++ ...ects_inheriting_apple_app_store_active.yml | 22 ++++ ...09213642_groups_apple_app_store_active.yml | 22 ++++ ...214020_projects_apple_app_store_active.yml | 22 ++++ ...oups_inheriting_apple_app_store_active.yml | 22 ++++ ...33201_instances_apple_app_store_active.yml | 22 ++++ doc/api/graphql/reference/index.md | 1 + doc/api/integrations.md | 38 ++++++ .../project/integrations/apple_app_store.md | 57 +++++++++ lib/api/helpers/integrations_helpers.rb | 21 ++++ locale/gitlab.pot | 21 ++++ spec/factories/integrations.rb | 10 ++ spec/lib/gitlab/import_export/all_models.yml | 1 + spec/models/ci/build_spec.rb | 46 ++++++++ .../integrations/apple_app_store_spec.rb | 104 ++++++++++++++++ .../integrations/every_integration_spec.rb | 4 +- spec/models/integrations/field_spec.rb | 2 + spec/models/project_spec.rb | 1 + .../integrations_shared_context.rb | 11 +- 28 files changed, 567 insertions(+), 6 deletions(-) create mode 100644 app/models/integrations/apple_app_store.rb create mode 100644 config/feature_flags/development/apple_app_store_integration.yml create mode 100644 config/metrics/counts_all/20221209212603_projects_inheriting_apple_app_store_active.yml create mode 100644 config/metrics/counts_all/20221209213642_groups_apple_app_store_active.yml create mode 100644 config/metrics/counts_all/20221209214020_projects_apple_app_store_active.yml create mode 100644 config/metrics/counts_all/20221209233053_groups_inheriting_apple_app_store_active.yml create mode 100644 config/metrics/counts_all/20221209233201_instances_apple_app_store_active.yml create mode 100644 doc/user/project/integrations/apple_app_store.md create mode 100644 spec/models/integrations/apple_app_store_spec.rb diff --git a/Gemfile b/Gemfile index 03b06c63fa2bee..3e58c6b6a64984 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 9c3173f19f40a8..7c46d84d4668ee 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 f12157122f95cc..94811ebb2ee096 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 74d998503b7330..1da612893ce8ef 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 34b5b637422fb4..d8a332ab12f787 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 dfa3642cfe4f7f..70920c2a3a9ff2 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -282,6 +282,7 @@ def self.available_integration_names(include_project_specific: true, include_dev 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) 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 00000000000000..d132146ec3e084 --- /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-Z]+[0-9]+/.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 53c8f5f623eb6e..329c046075f6ee 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 91527f9f76d976..9c56e152fb6f3a 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 00000000000000..f4d2a6aaa76487 --- /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.7' +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 00000000000000..0af2890d488be5 --- /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.7" +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 00000000000000..f01e889b5c2348 --- /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.7" +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 00000000000000..bb26a2beecbba9 --- /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.7" +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 00000000000000..f4abaee826522f --- /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.7" +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 00000000000000..c2bb671fe5e29e --- /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.7" +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 682a974fb4dd35..e23eb837071279 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 f6ad095aad6f47..24e0f189aad67d 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 00000000000000..4bd37152cc287d --- /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.7. + +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 543449c0349827..e2549c4bffbecf 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 a0cfd270916892..c65af7461d74f3 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 ebbf1b560e53c9..7740b2da911fc0 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 b34399d20f140f..8175b60c76724b 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 39ea3fafec38e1..36630488584c06 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 00000000000000..efbbbdf7b94bcd --- /dev/null +++ b/spec/models/integrations/apple_app_store_spec.rb @@ -0,0 +1,104 @@ +# 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('ABC1').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) } + 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 33e89b3dabcea6..8666ef512fcb02 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 642fb1fbf7ffaf..c30f9ef0d7b797 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 04edb755b58f81..3ce26906c27f3a 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 ca2fe8a6c54a20..63351a80648547 100644 --- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +Feature.enable(:apple_app_store_integration) Integration.available_integration_names.each do |integration| RSpec.shared_context integration do @@ -40,7 +41,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 +69,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 @@ -107,6 +114,8 @@ def enable_license_for_integration(integration) end end +Feature.disable(:apple_app_store_integration) + RSpec.shared_context 'integration activation' do def click_active_checkbox find('label', text: 'Active').click -- GitLab From 8c1901db2b29e30b860583b07e74502353f23aec Mon Sep 17 00:00:00 2001 From: Darby Frey Date: Mon, 19 Dec 2022 07:19:57 -0600 Subject: [PATCH 2/5] Adding include_feature_flagged option to available_integration_names --- app/models/integration.rb | 4 ++-- .../features/integrations/integrations_shared_context.rb | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/models/integration.rb b/app/models/integration.rb index 70920c2a3a9ff2..ee402d70e5c204 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -278,11 +278,11 @@ 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) + 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) + names << 'apple_app_store' if Feature.enabled?(:apple_app_store_integration) || include_feature_flagged names.sort_by(&:downcase) end 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 63351a80648547..005046166d87df 100644 --- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true -Feature.enable(:apple_app_store_integration) -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' @@ -114,8 +113,6 @@ def enable_license_for_integration(integration) end end -Feature.disable(:apple_app_store_integration) - RSpec.shared_context 'integration activation' do def click_active_checkbox find('label', text: 'Active').click -- GitLab From 7a822dd6cfc8ee3c7f5d9a6a6e07ffbee04b68a1 Mon Sep 17 00:00:00 2001 From: Darby Frey Date: Mon, 19 Dec 2022 09:40:07 -0600 Subject: [PATCH 3/5] Updated release milestone --- .../feature_flags/development/apple_app_store_integration.yml | 2 +- ...0221209212603_projects_inheriting_apple_app_store_active.yml | 2 +- .../counts_all/20221209213642_groups_apple_app_store_active.yml | 2 +- .../20221209214020_projects_apple_app_store_active.yml | 2 +- .../20221209233053_groups_inheriting_apple_app_store_active.yml | 2 +- .../20221209233201_instances_apple_app_store_active.yml | 2 +- doc/user/project/integrations/apple_app_store.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/config/feature_flags/development/apple_app_store_integration.yml b/config/feature_flags/development/apple_app_store_integration.yml index f4d2a6aaa76487..ec55f1ef9326a8 100644 --- a/config/feature_flags/development/apple_app_store_integration.yml +++ b/config/feature_flags/development/apple_app_store_integration.yml @@ -2,7 +2,7 @@ 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.7' +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 index 0af2890d488be5..5e00246a15ce55 100644 --- 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 @@ -7,7 +7,7 @@ product_group: integrations product_category: integrations value_type: number status: active -milestone: "15.7" +milestone: "15.8" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888 time_frame: all data_source: database 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 index f01e889b5c2348..9099752c62c286 100644 --- a/config/metrics/counts_all/20221209213642_groups_apple_app_store_active.yml +++ b/config/metrics/counts_all/20221209213642_groups_apple_app_store_active.yml @@ -7,7 +7,7 @@ product_group: integrations product_category: integrations value_type: number status: active -milestone: "15.7" +milestone: "15.8" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888 time_frame: all data_source: database 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 index bb26a2beecbba9..92e9acbcca0051 100644 --- a/config/metrics/counts_all/20221209214020_projects_apple_app_store_active.yml +++ b/config/metrics/counts_all/20221209214020_projects_apple_app_store_active.yml @@ -7,7 +7,7 @@ product_group: integrations product_category: integrations value_type: number status: active -milestone: "15.7" +milestone: "15.8" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888 time_frame: all data_source: database 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 index f4abaee826522f..f7835a4e072a4c 100644 --- 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 @@ -7,7 +7,7 @@ product_group: integrations product_category: integrations value_type: number status: active -milestone: "15.7" +milestone: "15.8" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888 time_frame: all data_source: database 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 index c2bb671fe5e29e..436f869cf0d2d8 100644 --- a/config/metrics/counts_all/20221209233201_instances_apple_app_store_active.yml +++ b/config/metrics/counts_all/20221209233201_instances_apple_app_store_active.yml @@ -7,7 +7,7 @@ product_group: integrations product_category: integrations value_type: number status: active -milestone: "15.7" +milestone: "15.8" introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888 time_frame: all data_source: database diff --git a/doc/user/project/integrations/apple_app_store.md b/doc/user/project/integrations/apple_app_store.md index 4bd37152cc287d..f381e71812c1e7 100644 --- a/doc/user/project/integrations/apple_app_store.md +++ b/doc/user/project/integrations/apple_app_store.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Apple App Store integration **(FREE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104888) in GitLab 15.7. +> [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. -- GitLab From ec65ff62f33da3b0c8c36cb40ca245939583ea06 Mon Sep 17 00:00:00 2001 From: Darby Frey Date: Tue, 27 Dec 2022 11:15:48 -0600 Subject: [PATCH 4/5] Updated KEY_ID_REGEX, added follow up issue for feature flags --- app/models/integration.rb | 2 ++ app/models/integrations/apple_app_store.rb | 2 +- spec/models/integrations/apple_app_store_spec.rb | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/integration.rb b/app/models/integration.rb index ee402d70e5c204..12616a7f39a70e 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -278,6 +278,8 @@ def self.nonexistent_integration_types_for(scope) # Returns a list of available integration names. # Example: ["asana", ...] # @deprecated + # @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 diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb index d132146ec3e084..b146452dd0fcf3 100644 --- a/app/models/integrations/apple_app_store.rb +++ b/app/models/integrations/apple_app_store.rb @@ -5,7 +5,7 @@ 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-Z]+[0-9]+/.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 } diff --git a/spec/models/integrations/apple_app_store_spec.rb b/spec/models/integrations/apple_app_store_spec.rb index efbbbdf7b94bcd..dde26e383c73c9 100644 --- a/spec/models/integrations/apple_app_store_spec.rb +++ b/spec/models/integrations/apple_app_store_spec.rb @@ -16,9 +16,10 @@ 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('ABC1').for(:app_store_key_id) } + 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 -- GitLab From cd1b8470962a615ffd899f90bbdfa67d3a388da1 Mon Sep 17 00:00:00 2001 From: Dominic Couture Date: Tue, 3 Jan 2023 20:50:42 +0000 Subject: [PATCH 5/5] Removing $ from variable names in help text --- app/models/integrations/apple_app_store.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/integrations/apple_app_store.rb b/app/models/integrations/apple_app_store.rb index b146452dd0fcf3..8418554293978f 100644 --- a/app/models/integrations/apple_app_store.rb +++ b/app/models/integrations/apple_app_store.rb @@ -41,9 +41,9 @@ def description def help variable_list = [ - '$APP_STORE_CONNECT_API_KEY_ISSUER_ID', - '$APP_STORE_CONNECT_API_KEY_KEY_ID', - '$APP_STORE_CONNECT_API_KEY_KEY' + 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', + 'APP_STORE_CONNECT_API_KEY_KEY_ID', + 'APP_STORE_CONNECT_API_KEY_KEY' ] # rubocop:disable Layout/LineLength -- GitLab