diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js index e9bcc789bcb228ce18fca4a83c05c830e48f065e..6b5a828c00901a6dc62fdf0c2f2812f7a2023bb3 100644 --- a/app/assets/javascripts/integrations/constants.js +++ b/app/assets/javascripts/integrations/constants.js @@ -33,6 +33,7 @@ export const integrationFormSections = { JIRA_ISSUES: 'jira_issues', TRIGGER: 'trigger', APPLE_APP_STORE: 'apple_app_store', + GOOGLE_PLAY: 'google_play', }; export const integrationFormSectionComponents = { @@ -42,6 +43,7 @@ export const integrationFormSectionComponents = { [integrationFormSections.JIRA_ISSUES]: 'IntegrationSectionJiraIssues', [integrationFormSections.TRIGGER]: 'IntegrationSectionTrigger', [integrationFormSections.APPLE_APP_STORE]: 'IntegrationSectionAppleAppStore', + [integrationFormSections.GOOGLE_PLAY]: 'IntegrationSectionGooglePlay', }; export const integrationTriggerEvents = { diff --git a/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue b/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue index d33f0ba317110dca154abfa909bb22255405f8e4..5335b7b6ee2c491bbcb51d20f21b45e1c9c5808e 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue @@ -32,6 +32,10 @@ export default { import( /* webpackChunkName: 'IntegrationSectionAppleAppStore' */ '~/integrations/edit/components/sections/apple_app_store.vue' ), + IntegrationSectionGooglePlay: () => + import( + /* webpackChunkName: 'IntegrationSectionGooglePlay' */ '~/integrations/edit/components/sections/google_play.vue' + ), }, directives: { SafeHtml, diff --git a/app/assets/javascripts/integrations/edit/components/sections/google_play.vue b/app/assets/javascripts/integrations/edit/components/sections/google_play.vue new file mode 100644 index 0000000000000000000000000000000000000000..3094e24241a62d852bf10b2e3917fc65cb947a52 --- /dev/null +++ b/app/assets/javascripts/integrations/edit/components/sections/google_play.vue @@ -0,0 +1,75 @@ + + + diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb index b8b93814e2b69794db53b6d4e175114e28b938be..7e1ba49d442d072a3304c08ed5cd300a08fef460 100644 --- a/app/controllers/concerns/integrations/params.rb +++ b/app/controllers/concerns/integrations/params.rb @@ -73,6 +73,8 @@ module Params :server, :server_host, :server_port, + :service_account_key, + :service_account_key_file_name, :sound, :subdomain, :teamcity_url, diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 0941f6d4cf0c82ab89275853dfcdc2c58ce62b61..0c2332d801208c21bcdc58d297c89edac7a89395 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -71,6 +71,7 @@ class Build < Ci::Processable delegate :gitlab_deploy_token, to: :project delegate :harbor_integration, to: :project delegate :apple_app_store_integration, to: :project + delegate :google_play_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 @@ -599,6 +600,7 @@ def persisted_variables .concat(deploy_token_variables) .concat(harbor_variables) .concat(apple_app_store_variables) + .concat(google_play_variables) end end @@ -649,6 +651,13 @@ def apple_app_store_variables Gitlab::Ci::Variables::Collection.new(apple_app_store_integration.ci_variables) end + def google_play_variables + return [] unless google_play_integration.try(:activated?) + return [] unless pipeline.protected_ref? + + Gitlab::Ci::Variables::Collection.new(google_play_integration.ci_variables) + end + def features { trace_sections: true, diff --git a/app/models/integration.rb b/app/models/integration.rb index 8bef8b08c196acbd42b656e3cd17a8a5a75ced76..6aa3ef526705a517faac824588d97094867c92fc 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -27,7 +27,7 @@ class Integration < ApplicationRecord # TODO Shimo is temporary disabled on group and instance-levels. # See: https://gitlab.com/gitlab-org/gitlab/-/issues/345677 PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[ - apple_app_store jenkins shimo + apple_app_store google_play jenkins shimo ].freeze # Fake integrations to help with local development. diff --git a/app/models/integrations/google_play.rb b/app/models/integrations/google_play.rb new file mode 100644 index 0000000000000000000000000000000000000000..8f1d2e7e1ecd08603d2dec1ecde74c982415a846 --- /dev/null +++ b/app/models/integrations/google_play.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Integrations + class GooglePlay < Integration + SECTION_TYPE_GOOGLE_PLAY = 'google_play' + + with_options if: :activated? do + validates :service_account_key, presence: true, json_schema: { + filename: "google_service_account_key", parse_json: true + } + validates :service_account_key_file_name, presence: true + end + + field :service_account_key_file_name, + section: SECTION_TYPE_CONNECTION, + required: true, + is_secret: false + + field :service_account_key, api_only: true, is_secret: false + + def title + s_('GooglePlay|Google Play') + end + + def description + s_('GooglePlay|Use GitLab to build and release an app in Google Play.') + end + + def help + variable_list = [ + 'SUPPLY_JSON_KEY_DATA' + ] + + # rubocop:disable Layout/LineLength + texts = [ + s_("Use the Google Play integration to connect to Google Play with fastlane in CI/CD pipelines."), + s_("After you enable the integration, the following protected variable is created for CI/CD use:"), + variable_list.join('
'), + s_(format("To generate a Google Play service account key and use this integration, see the integration documentation.", url: "#")).html_safe + ] + # rubocop:enable Layout/LineLength + + texts.join('

'.html_safe) + end + + def self.to_param + 'google_play' + end + + def self.supported_events + [] + end + + def sections + [ + { + type: SECTION_TYPE_GOOGLE_PLAY, + title: s_('Integrations|Integration details'), + description: help + } + ] + end + + def test(*_args) + client.fetch_access_token! + { success: true } + rescue Signet::AuthorizationError => error + { success: false, message: error } + end + + def ci_variables + return [] unless activated? + + [ + { key: 'SUPPLY_JSON_KEY_DATA', value: service_account_key, masked: true, public: false } + ] + end + + private + + def client + Google::Auth::ServiceAccountCredentials.make_creds( + json_key_io: StringIO.new(service_account_key), + scope: ['https://www.googleapis.com/auth/androidpublisher'] + ) + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index b1bb0ff2bbb7ec09d33c0e142a17f403ea17628b..c152c83c6a74209093a54990a4f515f31ebb1133 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -188,6 +188,7 @@ def self.integration_association_name(name) has_one :emails_on_push_integration, class_name: 'Integrations::EmailsOnPush' has_one :ewm_integration, class_name: 'Integrations::Ewm' has_one :external_wiki_integration, class_name: 'Integrations::ExternalWiki' + has_one :google_play_integration, class_name: 'Integrations::GooglePlay' has_one :hangouts_chat_integration, class_name: 'Integrations::HangoutsChat' has_one :harbor_integration, class_name: 'Integrations::Harbor' has_one :irker_integration, class_name: 'Integrations::Irker' @@ -1637,6 +1638,7 @@ def find_or_initialize_integrations def disabled_integrations disabled_integrations = [] disabled_integrations << 'apple_app_store' unless Feature.enabled?(:apple_app_store_integration, self) + disabled_integrations << 'google_play' unless Feature.enabled?(:google_play_integration, self) disabled_integrations end diff --git a/app/validators/json_schema_validator.rb b/app/validators/json_schema_validator.rb index 4896c2ea2efbab2800a117b487009c2e68bf1582..9c246a114f64d2fd966bffefc466e73237fb08ce 100644 --- a/app/validators/json_schema_validator.rb +++ b/app/validators/json_schema_validator.rb @@ -25,6 +25,7 @@ def initialize(options) def validate_each(record, attribute, value) value = value.to_h.stringify_keys if options[:hash_conversion] == true + value = Gitlab::Json.parse(value.to_s) if options[:parse_json] == true && !value.nil? unless valid_schema?(value) record.errors.add(attribute, _("must be a valid json schema")) diff --git a/app/validators/json_schemas/google_service_account_key.json b/app/validators/json_schemas/google_service_account_key.json new file mode 100644 index 0000000000000000000000000000000000000000..d040ef19f66f6a77a6969952e198adf2c3fa4f3d --- /dev/null +++ b/app/validators/json_schemas/google_service_account_key.json @@ -0,0 +1,48 @@ +{ + "description": "Google service account key", + "type": "object", + "required": [ + "type", + "project_id", + "private_key_id", + "private_key", + "client_email", + "client_id", + "auth_uri", + "token_uri", + "auth_provider_x509_cert_url", + "client_x509_cert_url" + ], + "properties": { + "type": { + "const": "service_account" + }, + "project_id": { + "type": "string" + }, + "private_key_id": { + "type": "string" + }, + "private_key": { + "type": "string" + }, + "client_email": { + "type": "string" + }, + "client_id": { + "type": "string" + }, + "auth_uri": { + "type": "string" + }, + "token_uri": { + "type": "string" + }, + "auth_provider_x509_cert_url": { + "type": "string" + }, + "client_x509_cert_url": { + "type": "string" + } + } +} diff --git a/config/feature_flags/development/google_play_integration.yml b/config/feature_flags/development/google_play_integration.yml new file mode 100644 index 0000000000000000000000000000000000000000..81c509cdab7583437e694edb7ab51802de6bfb9b --- /dev/null +++ b/config/feature_flags/development/google_play_integration.yml @@ -0,0 +1,8 @@ +--- +name: google_play_integration +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110440 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389611 +milestone: '15.10' +type: development +group: group::incubation +default_enabled: false diff --git a/config/metrics/counts_all/20230210184724_projects_inheriting_google_play_active.yml b/config/metrics/counts_all/20230210184724_projects_inheriting_google_play_active.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a24543390a6be21c926b7f4b8760e7fcedd7cf0 --- /dev/null +++ b/config/metrics/counts_all/20230210184724_projects_inheriting_google_play_active.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.projects_inheriting_google_play_active +description: Count of active projects inheriting integrations for Google Play +product_section: dev +product_stage: manage +product_group: integrations +product_category: integrations +value_type: number +status: active +milestone: "15.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621 +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/20230222192643_projects_google_play_active.yml b/config/metrics/counts_all/20230222192643_projects_google_play_active.yml new file mode 100644 index 0000000000000000000000000000000000000000..712a9b9cac1540802f67f673c5ac9c9dc6524523 --- /dev/null +++ b/config/metrics/counts_all/20230222192643_projects_google_play_active.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.projects_google_play_active +description: Count of projects with active integrations for Google Play +product_section: dev +product_stage: manage +product_group: integrations +product_category: integrations +value_type: number +status: active +milestone: "15.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621 +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/20230222193011_instances_google_play_active.yml b/config/metrics/counts_all/20230222193011_instances_google_play_active.yml new file mode 100644 index 0000000000000000000000000000000000000000..00f99ed13f4a4203a299f5444858b042ff2ebcea --- /dev/null +++ b/config/metrics/counts_all/20230222193011_instances_google_play_active.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.instances_google_play_active +description: Count of instances with active integrations for Google Play +product_section: dev +product_stage: manage +product_group: integrations +product_category: integrations +value_type: number +status: active +milestone: "15.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621 +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/20230222193151_groups_inheriting_google_play_active.yml b/config/metrics/counts_all/20230222193151_groups_inheriting_google_play_active.yml new file mode 100644 index 0000000000000000000000000000000000000000..1dad27560b69ce1e0e061f4951a4cedddd2058e3 --- /dev/null +++ b/config/metrics/counts_all/20230222193151_groups_inheriting_google_play_active.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.groups_inheriting_google_play_active +description: Count of active groups inheriting integrations for Google Play +product_section: dev +product_stage: manage +product_group: integrations +product_category: integrations +value_type: number +status: active +milestone: "15.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621 +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/20230222193255_groups_google_play_active.yml b/config/metrics/counts_all/20230222193255_groups_google_play_active.yml new file mode 100644 index 0000000000000000000000000000000000000000..fe83398f9ecb0da2a1ef72561235dd053137f0d3 --- /dev/null +++ b/config/metrics/counts_all/20230222193255_groups_google_play_active.yml @@ -0,0 +1,22 @@ +--- +key_path: counts.groups_google_play_active +description: Count of active groups inheriting integrations for Google Play +product_section: dev +product_stage: manage +product_group: integrations +product_category: integrations +value_type: number +status: active +milestone: "15.10" +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621 +time_frame: all +data_source: database +data_category: optional +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/db/docs/integrations.yml b/db/docs/integrations.yml index 5bb4f4485417510a48759344558afd0583aaeb74..c1b18b29804ea935df39d1069c3c62bfee38ca47 100644 --- a/db/docs/integrations.yml +++ b/db/docs/integrations.yml @@ -26,6 +26,7 @@ classes: - Integrations::ExternalWiki - Integrations::Github - Integrations::GitlabSlackApplication +- Integrations::GooglePlay - Integrations::HangoutsChat - Integrations::Harbor - Integrations::Irker diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index a4696acdfb33fdcd07a3a1d9be624da3b3e08cdf..5dd3886d5c52f791763dd8fff3783c68aae441be 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -23597,6 +23597,7 @@ State of a Sentry error. | `EXTERNAL_WIKI_SERVICE` | ExternalWikiService type. | | `GITHUB_SERVICE` | GithubService type. | | `GITLAB_SLACK_APPLICATION_SERVICE` | GitlabSlackApplicationService type (Gitlab.com only). | +| `GOOGLE_PLAY_SERVICE` | GooglePlayService type. | | `HANGOUTS_CHAT_SERVICE` | HangoutsChatService type. | | `HARBOR_SERVICE` | HarborService type. | | `IRKER_SERVICE` | IrkerService type. | diff --git a/doc/user/project/integrations/google_play.md b/doc/user/project/integrations/google_play.md new file mode 100644 index 0000000000000000000000000000000000000000..553e82be3828cc931590e69c0621e3a4f2e4caf3 --- /dev/null +++ b/doc/user/project/integrations/google_play.md @@ -0,0 +1,49 @@ +--- +stage: Manage +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 +--- + +# Google Play **(FREE)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111621) in GitLab 15.10 [with a flag](../../../administration/feature_flags.md) named `google_play_integration`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `google_play_integration`. On GitLab.com, this feature is not available. + +With the Google Play integration, you can configure your CI/CD pipelines to connect to the [Google Play Console](https://play.google.com/console) to build and release apps for Android devices. + +The Google Play integration works out of the box with [fastlane](https://fastlane.tools/). You can also use this integration with other build tools. + +## Enable the integration in GitLab + +Prerequisites: + +- You must have a [Google Play Console](https://play.google.com/console/signup) developer account. +- You must [generate a new service account key for your project](https://developers.google.com/android-publisher/getting_started) from the Google Cloud console. + +To enable the Google Play integration in GitLab: + +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Integrations**. +1. Select **Google Play**. +1. In **Enable Integration**, select the **Active** checkbox. +1. In **Service account key (.JSON)**, drag or upload your key file. +1. Select **Save changes**. + +After you enable the integration, the global variable `$SUPPLY_JSON_KEY_DATA` is created for CI/CD use. + +### CI/CD variable security + +Malicious code pushed to your `.gitlab-ci.yml` file could compromise your variables, including `$SUPPLY_JSON_KEY_DATA`, and send them to a third-party server. For more information, see [CI/CD variable security](../../../ci/variables/index.md#cicd-variable-security). + +## Enable the integration in fastlane + +To enable the integration in fastlane and upload the build to the given track in Google Play, you can add the following code to your app's `fastlane/Fastfile`: + +```ruby +upload_to_play_store( + track: 'internal', + aab: '../build/app/outputs/bundle/release/app-release.aab' +) +``` diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb index d13a78db01cd0ca05d23ceed15b744c919f7c5cf..60cb61ef02f74403278dfdce8acc159f400ebdbf 100644 --- a/lib/api/helpers/integrations_helpers.rb +++ b/lib/api/helpers/integrations_helpers.rb @@ -453,6 +453,20 @@ def self.integrations desc: 'The URL of the external wiki' } ], + 'google-play' => [ + { + required: true, + name: :service_account_key, + type: String, + desc: 'The Google Play Service Account Key' + }, + { + required: true, + name: :service_account_key_file_name, + type: String, + desc: 'The Google Play Service Account Key File Name' + } + ], 'hangouts-chat' => [ { required: true, @@ -924,6 +938,7 @@ def self.integration_classes ::Integrations::EmailsOnPush, ::Integrations::Ewm, ::Integrations::ExternalWiki, + ::Integrations::GooglePlay, ::Integrations::HangoutsChat, ::Integrations::Harbor, ::Integrations::Irker, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 85484b1505e0e91f12e02553cf2f13d26e3b0c6a..2ad9741e3e8c4be7dbe6fbbcdaf4e86911a8a507 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3705,6 +3705,9 @@ 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 "" +msgid "After you enable the integration, the following protected variable is created for CI/CD use:" +msgstr "" + msgid "After you've reviewed these contribution guidelines, you'll be all set to" msgstr "" @@ -19649,6 +19652,30 @@ msgstr "" msgid "GoogleCloud|Revoke authorizations granted to GitLab. This does not invalidate service accounts." msgstr "" +msgid "GooglePlay|Drag your key file here or %{linkStart}click to upload%{linkEnd}." +msgstr "" + +msgid "GooglePlay|Drag your key file to start the upload." +msgstr "" + +msgid "GooglePlay|Error: The file you're trying to upload is not a service account key." +msgstr "" + +msgid "GooglePlay|Google Play" +msgstr "" + +msgid "GooglePlay|Leave empty to use your current service account key." +msgstr "" + +msgid "GooglePlay|Service account key (.json)" +msgstr "" + +msgid "GooglePlay|Upload a new service account key (replace %{currentFileName})" +msgstr "" + +msgid "GooglePlay|Use GitLab to build and release an app in Google Play." +msgstr "" + msgid "Got it" msgstr "" @@ -46426,6 +46453,9 @@ 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 Google Play integration to connect to Google Play 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 dccca9ce30decf09af91e5a97466da15ac199541..5a666e57ad1860656bb439d186d7004aea87b491 100644 --- a/spec/factories/integrations.rb +++ b/spec/factories/integrations.rb @@ -265,6 +265,15 @@ app_store_private_key { File.read('spec/fixtures/auth_key.p8') } end + factory :google_play_integration, class: 'Integrations::GooglePlay' do + project + active { true } + type { 'Integrations::GooglePlay' } + + service_account_key_file_name { 'service_account.json' } + service_account_key { File.read('spec/fixtures/service_account.json') } + 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/features/projects/integrations/google_play_spec.rb b/spec/features/projects/integrations/google_play_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5db4bc8809fa5ca834fedf1876f58ea29cc1e34c --- /dev/null +++ b/spec/features/projects/integrations/google_play_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Upload Dropzone Field', feature_category: :integrations do + include_context 'project integration activation' + + it 'uploads the file data to the correct form fields and updates the messaging correctly', :js, :aggregate_failures do + visit_project_integration('Google Play') + + expect(page).to have_content('Drag your key file here or click to upload.') + expect(page).not_to have_content('service_account.json') + + find("input[name='service[dropzone_file_name]']", + visible: false).set(Rails.root.join('spec/fixtures/service_account.json')) + + expect(find("input[name='service[service_account_key]']", + visible: false).value).to eq(File.read(Rails.root.join('spec/fixtures/service_account.json'))) + expect(find("input[name='service[service_account_key_file_name]']", + visible: false).value).to eq('service_account.json') + + expect(page).not_to have_content('Drag your key file here or click to upload.') + expect(page).to have_content('service_account.json') + end +end diff --git a/spec/fixtures/service_account.json b/spec/fixtures/service_account.json new file mode 100644 index 0000000000000000000000000000000000000000..9f7f5526cf52643dae265f1e118641c2df4a0291 --- /dev/null +++ b/spec/fixtures/service_account.json @@ -0,0 +1,12 @@ +{ + "type": "service_account", + "project_id": "demo-app-123", + "private_key_id": "47f0b1700983da548af6fcd37007f42996099999", + "private_key": "-----BEGIN PRIVATE KEY-----\nABCDEFIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJn8w20WcN+fi5\nIhO1BEFCv7ExK8J5rW5Pc8XpJgpQoL5cfv6qC6aS+x4maI7S4AG7diqXBLCfjlnA\nqBzXwCRnnPtQhu+v1ehAj5fGNa7F51f9aacRNmKdHzNmWZEPDuLqq0I/Ewcsotu+\nnb+tCYk1o2ahyPZau8JtXFZs7oZb7SrfgoSJemccxeVreGm1Dt6SM74/3qJAeHN/\niK/v0IiQP1GS4Jxgz38XQGo+jiTpNrFcf4S0RNxKcNf+tuuEBDi57LBLwdotM7E5\nF1l9pZZMWkmQKQIxeER6+2HuE56V6QPITwkQ/u9XZFQSgl4SBIw2sHr5D/xaUxjw\n+kMy2Jt9AgMBAAECggEACL7E34rRIWbP043cv3ZQs1RiWzY2mvWmCiMEzkz0rRRv\nyqNv0yXVYtzVV7KjdpY56leLgjM1Sv0PEQoUUtpWFJAXSXdKLaewSXPrpXCoz5OD\nekMgeItnQcE7nECdyAKsCSQw/SXg4t4p0a3WGsCwt3If2TwWIrov9R4zGcn1wMZn\n922WtZDmh2NqdTZIKElWZLxNlIr/1v88mAp7oSa1DLfqWkwEEnxK7GGAiwN8ARIF\nkvgiuKdsHBf5aNKg70xN6AcZx/Z4+KZxXxyKKF5VkjCtDzA97EjJqftDPwGTkela\n2bgiDSJs0Un0wQpFFRDrlfyo7rr9Ey/Gf4rR66NWeQKBgQD7qPP55xoWHCDvoK9P\nMN67qFLNDPWcKVKr8siwUlZ6/+acATXjfNUjsJLM7vBxYLjdtFxQ/vojJTQyMxHt\n80wARDk1DTu2zhltL2rKo6LfbwjQsot1MLZFXAMwqtHTLfURaj8kO1JDV/j+4a94\nP0gzNMiBYAKWm6z08akEz2TrhQKBgQDNGfFvtxo4Mf6AA3iYXCwc0CJXb+cqZkW/\n7glnV+vDqYVo23HJaKHFD+Xqaj+cUrOUNglWgT9WSCZR++Hzw1OCPZvX2V9Z6eQh\ngqOBX6D19q9jfShfxLywEAD5pk7LMINumsNm6H+6shJQK5c67bsM9/KQbSnIlWhw\n7JBe8OlFmQKBgQDREyF2mb/7ZG0ch8N9qB0zjHkV79FRZqdPQUnn6s/8KgO90eei\nUkCFARpE9bF+kBul3UTg6aSIdE0z82fO51VZ11Qrtg3JJtrK8hznsyEKPaX2NI9V\n0h1r7DCeSxw9NS4nxLwmbr4+QqUTpA3yeaiTGiQGD+y2kSkU6nxACclPPQKBgFkb\nkVqg6YJKrjB90ZIYUY3/GzxzwLIaFumpCGretu6eIvkIhiokDExqeNBccuB+ych1\npZ7wrkzVMdjinythzFFEZQXlSdjtlhC9Cj52Bp92GoMV6EmbVwMDIPlVuNvsat3N\n3WFDV+ML5IryNVUD3gVnX/pBgyrDRsnw7VRiRGbZAoGBANxZwGKZo0zpyb5O5hS6\nxVrgJtIySlV5BOEjFXKeLwzByht8HmrHhSWix6WpPejfK1RHhl3boU6t9yeC0cre\nvUI/Y9LBhHXjSwWCWlqVe9yYqsde+xf0UYRS8IoaoJjus7YVJr9yPpCboEF28ZmQ\ndVBlpZYg6oLIar6waaLMz/1B\n-----END PRIVATE KEY-----\n", + "client_email": "demo-app-account@demo-app-374914.iam.gserviceaccount.com", + "client_id": "111111116847110173051", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/demo-app-account%40demo-app-374914.iam.gserviceaccount.com" +} diff --git a/spec/frontend/integrations/edit/components/sections/google_play_spec.js b/spec/frontend/integrations/edit/components/sections/google_play_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..c0d6d17f639edd0acba913e52cb6b960f8464bf1 --- /dev/null +++ b/spec/frontend/integrations/edit/components/sections/google_play_spec.js @@ -0,0 +1,54 @@ +import { shallowMount } from '@vue/test-utils'; + +import IntegrationSectionGooglePlay from '~/integrations/edit/components/sections/google_play.vue'; +import UploadDropzoneField from '~/integrations/edit/components/upload_dropzone_field.vue'; +import { createStore } from '~/integrations/edit/store'; + +describe('IntegrationSectionGooglePlay', () => { + let wrapper; + + const createComponent = (fileName = '') => { + const store = createStore({ + customState: { + fields: [ + { + name: 'service_account_key_file_name', + value: fileName, + }, + ], + }, + }); + + wrapper = shallowMount(IntegrationSectionGooglePlay, { + store, + }); + }; + + const findUploadDropzoneField = () => wrapper.findComponent(UploadDropzoneField); + + describe('computed properties', () => { + it('renders UploadDropzoneField with default values', () => { + createComponent(); + + const field = findUploadDropzoneField(); + + expect(field.exists()).toBe(true); + expect(field.props()).toMatchObject({ + label: 'Service account key (.json)', + helpText: '', + }); + }); + + it('renders UploadDropzoneField with custom values for an attached file', () => { + createComponent('fileName.txt'); + + const field = findUploadDropzoneField(); + + expect(field.exists()).toBe(true); + expect(field.props()).toMatchObject({ + label: 'Upload a new service account key (replace fileName.txt)', + helpText: 'Leave empty to use your current service account key.', + }); + }); + }); +}); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 056dc1f1966386864710e05740117f34c1122cac..490be954792effef68cc34b5cef9fc4e9185d70a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -532,6 +532,7 @@ project: - discord_integration - drone_ci_integration - emails_on_push_integration +- google_play_integration - pipelines_email_integration - mattermost_slash_commands_integration - shimo_integration diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 492768df14e96a7fc22eb35eb625e42f3dd9ea4e..99c168de6165f48a64200092d35a32e45ea9fa25 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -3639,6 +3639,46 @@ end end + context 'for the google_play integration' do + let_it_be(:google_play_integration) { create(:google_play_integration) } + + let(:google_play_variables) do + [ + { key: 'SUPPLY_JSON_KEY_DATA', value: google_play_integration.service_account_key, masked: true, public: false } + ] + end + + context 'when the google_play integration exists' do + context 'when a build is protected' do + before do + allow(build.pipeline).to receive(:protected_ref?).and_return(true) + build.project.update!(google_play_integration: google_play_integration) + end + + it 'includes google_play variables' do + is_expected.to include(*google_play_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!(google_play_integration: google_play_integration) + end + + it 'does not include the google_play variable' do + expect(subject[:key] == 'SUPPLY_JSON_KEY_DATA').to eq(false) + end + end + end + + context 'when the googel_play integration does not exist' do + it 'does not include google_play variable' do + expect(subject[:key] == 'SUPPLY_JSON_KEY_DATA').to eq(false) + 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/google_play_spec.rb b/spec/models/integrations/google_play_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ab1aaad24e7be31d6e3717419e0e18749f308a9e --- /dev/null +++ b/spec/models/integrations/google_play_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integrations::GooglePlay, 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 :service_account_key_file_name } + it { is_expected.to validate_presence_of :service_account_key } + it { is_expected.to allow_value(File.read('spec/fixtures/service_account.json')).for(:service_account_key) } + it { is_expected.not_to allow_value(File.read('spec/fixtures/group.json')).for(:service_account_key) } + end + end + + context 'when integration is enabled' do + let(:google_play_integration) { build(:google_play_integration) } + + describe '#fields' do + it 'returns custom fields' do + expect(google_play_integration.fields.pluck(:name)).to match_array(%w[service_account_key + service_account_key_file_name]) + end + end + + describe '#test' do + it 'returns true for a successful request' do + allow(Google::Auth::ServiceAccountCredentials).to receive_message_chain(:make_creds, :fetch_access_token!) + expect(google_play_integration.test[:success]).to be true + end + + it 'returns false for an invalid request' do + allow(Google::Auth::ServiceAccountCredentials).to receive_message_chain(:make_creds, + :fetch_access_token!).and_raise(Signet::AuthorizationError.new('error')) + expect(google_play_integration.test[:success]).to be false + end + end + + describe '#help' do + it 'renders prompt information' do + expect(google_play_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('google_play') + end + end + + describe '#ci_variables' do + let(:google_play_integration) { build_stubbed(:google_play_integration) } + + it 'returns vars when the integration is activated' do + ci_vars = [ + { + key: 'SUPPLY_JSON_KEY_DATA', + value: google_play_integration.service_account_key, + masked: true, + public: false + } + ] + + expect(google_play_integration.ci_variables).to match_array(ci_vars) + end + end + end + + context 'when integration is disabled' do + let(:google_play_integration) { build_stubbed(:google_play_integration, active: false) } + + describe '#ci_variables' do + it 'returns an empty array' do + expect(google_play_integration.ci_variables).to match_array([]) + end + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9db1dde6294682efa098c9535fdd69c460e3c28d..346948bb0263ea350d566a61be89a80e5a75d9df 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -50,6 +50,7 @@ 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(:google_play_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 178dad627eda6a649ecf1c87d2a47d7248cccd18..f07e4f6c9c37a542b4640eaabb6def954e81ef42 100644 --- a/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb +++ b/spec/support/shared_contexts/features/integrations/integrations_shared_context.rb @@ -76,6 +76,10 @@ hash.merge!(k => 'ABC1') elsif integration == 'apple_app_store' && k == :app_store_private_key_file_name hash.merge!(k => 'ssl_key.pem') + elsif integration == 'google_play' && k == :service_account_key + hash.merge!(k => File.read('spec/fixtures/service_account.json')) + elsif integration == 'google_play' && k == :service_account_key_file_name + hash.merge!(k => 'service_account.json') else hash.merge!(k => "someword") end