diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue index 6dc0b2cec24..11482efbb40 100644 --- a/app/assets/javascripts/import_entities/components/import_status.vue +++ b/app/assets/javascripts/import_entities/components/import_status.vue @@ -7,21 +7,21 @@ import { STATUSES } from '../constants'; const STATISTIC_ITEMS = { diff_note: __('Diff notes'), issue: __('Issues'), - issue_attachment: s__('GithubImporter|Issue attachments'), + issue_attachment: s__('GithubImporter|Issue links'), issue_event: __('Issue events'), label: __('Labels'), lfs_object: __('LFS objects'), - merge_request_attachment: s__('GithubImporter|Merge request attachments'), + merge_request_attachment: s__('GithubImporter|Merge request links'), milestone: __('Milestones'), note: __('Notes'), - note_attachment: s__('GithubImporter|Note attachments'), + note_attachment: s__('GithubImporter|Note links'), protected_branch: __('Protected branches'), pull_request: s__('GithubImporter|Pull requests'), pull_request_merged_by: s__('GithubImporter|PR mergers'), pull_request_review: s__('GithubImporter|PR reviews'), pull_request_review_request: s__('GithubImporter|PR reviews'), release: __('Releases'), - release_attachment: s__('GithubImporter|Release attachments'), + release_attachment: s__('GithubImporter|Release links'), }; // support both camel case and snake case versions diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js index e9bcc789bcb..6b5a828c009 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 d33f0ba3171..5335b7b6ee2 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 00000000000..3094e24241a --- /dev/null +++ b/app/assets/javascripts/integrations/edit/components/sections/google_play.vue @@ -0,0 +1,75 @@ + + + diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 20cf21cd1b6..eef011db7d2 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -208,7 +208,7 @@ export default { v-if="note.last_edited_at" :edited-at="note.last_edited_at" :edited-by="note.last_edited_by" - action-text="Edited" + :action-text="__('Edited')" class="note_edited_ago" /> -/* eslint-disable @gitlab/vue-require-i18n-strings */ +import { GlSprintf, GlLink } from '@gitlab/ui'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { EDITED_TEXT } from '../i18n'; export default { name: 'EditedNoteText', components: { + GlSprintf, + GlLink, TimeAgoTooltip, }, props: { @@ -33,19 +36,33 @@ export default { default: 'edited-text', }, }, + i18n: EDITED_TEXT, }; diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js index a758a55014a..8aad3ec07cb 100644 --- a/app/assets/javascripts/notes/i18n.js +++ b/app/assets/javascripts/notes/i18n.js @@ -49,3 +49,8 @@ export const COMMENT_FORM = { 'Notes|Attachments are sent by email. Attachments over 10 MB are sent as links to your GitLab instance, and only accessible to project members.', ), }; + +export const EDITED_TEXT = { + actionWithAuthor: __('%{actionText} by %{author} %{actionDetail}'), + actionWithoutAuthor: __('%{actionText} %{actionDetail}'), +}; diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb index b8b93814e2b..7e1ba49d442 100644 --- a/app/controllers/concerns/integrations/params.rb +++ b/app/controllers/concerns/integrations/params.rb @@ -73,6 +73,8 @@ module Integrations :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 0941f6d4cf0..0c2332d8012 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -71,6 +71,7 @@ module Ci 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 @@ module Ci .concat(deploy_token_variables) .concat(harbor_variables) .concat(apple_app_store_variables) + .concat(google_play_variables) end end @@ -649,6 +651,13 @@ module Ci 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 8bef8b08c19..6aa3ef52670 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 00000000000..8f1d2e7e1ec --- /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 f363f189178..20011b073ee 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -189,6 +189,7 @@ class Project < ApplicationRecord 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' @@ -1634,6 +1635,7 @@ class Project < ApplicationRecord 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 4896c2ea2ef..9c246a114f6 100644 --- a/app/validators/json_schema_validator.rb +++ b/app/validators/json_schema_validator.rb @@ -25,6 +25,7 @@ class JsonSchemaValidator < ActiveModel::EachValidator 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 00000000000..d040ef19f66 --- /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 00000000000..81c509cdab7 --- /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 00000000000..9a24543390a --- /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 00000000000..712a9b9cac1 --- /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 00000000000..00f99ed13f4 --- /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 00000000000..1dad27560b6 --- /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 00000000000..fe83398f9ec --- /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 5bb4f448541..c1b18b29804 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 83199d581c6..2af77ef5436 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -23762,6 +23762,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 00000000000..553e82be382 --- /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 d13a78db01c..60cb61ef02f 100644 --- a/lib/api/helpers/integrations_helpers.rb +++ b/lib/api/helpers/integrations_helpers.rb @@ -453,6 +453,20 @@ module API 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 @@ module API ::Integrations::EmailsOnPush, ::Integrations::Ewm, ::Integrations::ExternalWiki, + ::Integrations::GooglePlay, ::Integrations::HangoutsChat, ::Integrations::Harbor, ::Integrations::Irker, diff --git a/lib/gitlab/github_import/importer/note_attachments_importer.rb b/lib/gitlab/github_import/importer/note_attachments_importer.rb index 9901c9e76f5..a84fcd253ef 100644 --- a/lib/gitlab/github_import/importer/note_attachments_importer.rb +++ b/lib/gitlab/github_import/importer/note_attachments_importer.rb @@ -19,7 +19,7 @@ module Gitlab return if attachments.blank? new_text = attachments.reduce(note_text.text) do |text, attachment| - new_url = download_attachment(attachment) + new_url = gitlab_attachment_link(attachment) text.gsub(attachment.url, new_url) end @@ -28,6 +28,28 @@ module Gitlab private + def gitlab_attachment_link(attachment) + project_import_source = project.import_source + + if attachment.part_of_project_blob?(project_import_source) + convert_project_content_link(attachment.url, project_import_source) + elsif attachment.media? || attachment.doc_belongs_to_project?(project_import_source) + download_attachment(attachment) + else # url to other GitHub project + attachment.url + end + end + + # From: https://github.com/login/test-import-attachments-source/blob/main/example.md + # To: https://gitlab.com/login/test-import-attachments-target/-/blob/main/example.md + def convert_project_content_link(attachment_url, import_source) + path_without_domain = attachment_url.gsub(::Gitlab::GithubImport::MarkdownText.github_url, '') + path_without_import_source = path_without_domain.gsub(import_source, '').delete_prefix('/') + path_with_blob_prefix = "/-#{path_without_import_source}" + + ::Gitlab::Routing.url_helpers.project_url(project) + path_with_blob_prefix + end + # in: an instance of Gitlab::GithubImport::Markdown::Attachment # out: gitlab attachment markdown url def download_attachment(attachment) diff --git a/lib/gitlab/github_import/markdown/attachment.rb b/lib/gitlab/github_import/markdown/attachment.rb index 1c814e34a39..e270cfba619 100644 --- a/lib/gitlab/github_import/markdown/attachment.rb +++ b/lib/gitlab/github_import/markdown/attachment.rb @@ -79,6 +79,22 @@ module Gitlab @url = url end + def part_of_project_blob?(import_source) + url.start_with?( + "#{::Gitlab::GithubImport::MarkdownText.github_url}/#{import_source}/blob" + ) + end + + def doc_belongs_to_project?(import_source) + url.start_with?( + "#{::Gitlab::GithubImport::MarkdownText.github_url}/#{import_source}/files" + ) + end + + def media? + url.start_with?(::Gitlab::GithubImport::MarkdownText::GITHUB_MEDIA_CDN) + end + def inspect "<#{self.class.name}: { name: #{name}, url: #{url} }>" end diff --git a/lib/gitlab/github_import/settings.rb b/lib/gitlab/github_import/settings.rb index 77288b9fb98..22ab99df107 100644 --- a/lib/gitlab/github_import/settings.rb +++ b/lib/gitlab/github_import/settings.rb @@ -18,9 +18,9 @@ module Gitlab TEXT }, attachments_import: { - label: 'Import Markdown attachments', + label: 'Import Markdown attachments (links)', details: <<-TEXT.split("\n").map(&:strip).join(' ') - Import Markdown attachments from repository comments, release posts, issue descriptions, + Import Markdown attachments (links) from repository comments, release posts, issue descriptions, and pull request descriptions. These can include images, text, or binary attachments. If not imported, links in Markdown to attachments break after you remove the attachments from GitHub. TEXT diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb index b620e9b4560..de952b37b39 100644 --- a/lib/gitlab/url_blocker.rb +++ b/lib/gitlab/url_blocker.rb @@ -49,8 +49,13 @@ module Gitlab ascii_only: ascii_only ) - address_info = get_address_info(uri, dns_rebind_protection) - return [uri, nil] unless address_info + begin + address_info = get_address_info(uri) + rescue SocketError + return [uri, nil] unless enforce_address_info_retrievable?(uri, dns_rebind_protection) + + raise BlockedUrlError, 'Host cannot be resolved or invalid' + end ip_address = ip_address(address_info) return [uri, nil] if domain_allowed?(uri) @@ -115,24 +120,17 @@ module Gitlab validate_unicode_restriction(uri) if ascii_only end - def get_address_info(uri, dns_rebind_protection) + # Returns addrinfo object for the URI. + # + # @param uri [Addressable::URI] + # + # @raise [Gitlab::UrlBlocker::BlockedUrlError, ArgumentError] - BlockedUrlError raised if host is too long. + # + # @return [Array] + def get_address_info(uri) Addrinfo.getaddrinfo(uri.hostname, get_port(uri), nil, :STREAM).map do |addr| addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr end - rescue SocketError - # If the dns rebinding protection is not enabled or the domain - # is allowed, or HTTP_PROXY is set we avoid the dns rebinding checks - return if domain_allowed?(uri) || !dns_rebind_protection || Gitlab.http_proxy_env? - - # In the test suite we use a lot of mocked urls that are either invalid or - # don't exist. In order to avoid modifying a ton of tests and factories - # we allow invalid urls unless the environment variable RSPEC_ALLOW_INVALID_URLS - # is not true - return if Rails.env.test? && ENV['RSPEC_ALLOW_INVALID_URLS'] == 'true' - - # If the addr can't be resolved or the url is invalid (i.e http://1.1.1.1.1) - # we block the url - raise BlockedUrlError, "Host cannot be resolved or invalid" rescue ArgumentError => error # Addrinfo.getaddrinfo errors if the domain exceeds 1024 characters. raise unless error.message.include?('hostname too long') @@ -140,6 +138,18 @@ module Gitlab raise BlockedUrlError, "Host is too long (maximum is 1024 characters)" end + def enforce_address_info_retrievable?(uri, dns_rebind_protection) + return false if !dns_rebind_protection || Gitlab.http_proxy_env? || domain_allowed?(uri) + + # In the test suite we use a lot of mocked urls that are either invalid or + # don't exist. In order to avoid modifying a ton of tests and factories + # we allow invalid urls unless the environment variable RSPEC_ALLOW_INVALID_URLS + # is not true + return false if Rails.env.test? && ENV['RSPEC_ALLOW_INVALID_URLS'] == 'true' + + true + end + def validate_local_request( address_info:, allow_localhost:, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 900c7682c21..cdcb2296900 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -476,6 +476,9 @@ msgid_plural "%s additional commits have been omitted to prevent performance iss msgstr[0] "" msgstr[1] "" +msgid "%{actionText} %{actionDetail}" +msgstr "" + msgid "%{actionText} & %{openOrClose} %{noteable}" msgstr "" @@ -485,6 +488,9 @@ msgstr "" msgid "%{actionText} & reopen %{noteable}" msgstr "" +msgid "%{actionText} by %{author} %{actionDetail}" +msgstr "" + msgid "%{address} is an invalid IP address range" msgstr "" @@ -3716,6 +3722,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 "" @@ -19255,13 +19264,13 @@ msgstr "" msgid "GithubImporter|GitHub gists with more than 10 files must be manually migrated." msgstr "" -msgid "GithubImporter|Issue attachments" +msgid "GithubImporter|Issue links" msgstr "" -msgid "GithubImporter|Merge request attachments" +msgid "GithubImporter|Merge request links" msgstr "" -msgid "GithubImporter|Note attachments" +msgid "GithubImporter|Note links" msgstr "" msgid "GithubImporter|PR mergers" @@ -19279,7 +19288,7 @@ msgstr "" msgid "GithubImporter|Pull requests" msgstr "" -msgid "GithubImporter|Release attachments" +msgid "GithubImporter|Release links" msgstr "" msgid "GithubImporter|Your import of GitHub gists into GitLab snippets is complete." @@ -19687,6 +19696,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 "" @@ -46533,6 +46566,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/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb index 127db36052f..8c20c2cc0e2 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb @@ -37,7 +37,7 @@ module QA let!(:source_mr_approvers) { [source_admin_user.email] } let(:source_mr_comments) do source_mr.comments.map do |note| - { **note.except(:id, :noteable_id), author: note[:author].except(:web_url) } + { **note.except(:id, :noteable_id, :project_id), author: note[:author].except(:web_url) } end end @@ -52,11 +52,11 @@ module QA let(:imported_mr_comments) do imported_mr.comments.map do |note| - { **note.except(:id, :noteable_id), author: note[:author].except(:web_url) } + { **note.except(:id, :noteable_id, :project_id), author: note[:author].except(:web_url) } end end - let(:imported_mr_reviewers) { imported_mr.reviewers.map { |reviewer| reviewer[:username] } } + let(:imported_mr_reviewers) { imported_mr.reviewers.pluck(:username) } let(:imported_mr_approvers) do imported_mr.approval_configuration[:approved_by].map { |usr| usr.dig(:user, :username) } end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb index 815a8696ff7..b8a018552c6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', product_group: :source_code, quarantine: { - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/352525', - type: :test_environment, - only: { job: 'review-qa-*' } - } do + RSpec.describe 'Create', product_group: :source_code do describe 'Push mirror a repository over HTTP' do it 'configures and syncs LFS objects for a (push) mirrored repository', :aggregate_failures, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347847' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/spec/factories/integrations.rb b/spec/factories/integrations.rb index dccca9ce30d..5a666e57ad1 100644 --- a/spec/factories/integrations.rb +++ b/spec/factories/integrations.rb @@ -265,6 +265,15 @@ FactoryBot.define do 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 00000000000..5db4bc8809f --- /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 00000000000..9f7f5526cf5 --- /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 00000000000..c0d6d17f639 --- /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/frontend/notes/components/note_edited_text_spec.js b/spec/frontend/notes/components/note_edited_text_spec.js index 0a5fe48ef94..e76a1de0a66 100644 --- a/spec/frontend/notes/components/note_edited_text_spec.js +++ b/spec/frontend/notes/components/note_edited_text_spec.js @@ -1,3 +1,4 @@ +import { GlSprintf, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import NoteEditedText from '~/notes/components/note_edited_text.vue'; @@ -5,41 +6,67 @@ const propsData = { actionText: 'Edited', className: 'foo-bar', editedAt: '2017-08-04T09:52:31.062Z', - editedBy: { - avatar_url: 'path', - id: 1, - name: 'Root', - path: '/root', - state: 'active', - username: 'root', - }, + editedBy: null, }; describe('NoteEditedText', () => { let wrapper; - beforeEach(() => { + const createWrapper = (props = {}) => { wrapper = shallowMount(NoteEditedText, { - propsData, + propsData: { + ...propsData, + ...props, + }, + stubs: { + GlSprintf, + }, }); - }); + }; + + const findUserElement = () => wrapper.findComponent(GlLink); afterEach(() => { wrapper.destroy(); }); - it('should render block with provided className', () => { - expect(wrapper.classes()).toContain(propsData.className); + describe('default', () => { + beforeEach(() => { + createWrapper(); + }); + + it('should render block with provided className', () => { + expect(wrapper.classes()).toContain(propsData.className); + }); + + it('should render provided actionText', () => { + expect(wrapper.text().trim()).toContain(propsData.actionText); + }); + + it('should not render user information', () => { + expect(findUserElement().exists()).toBe(false); + }); }); - it('should render provided actionText', () => { - expect(wrapper.text().trim()).toContain(propsData.actionText); - }); + describe('edited note', () => { + const editedBy = { + avatar_url: 'path', + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }; - it('should render provided user information', () => { - const authorLink = wrapper.find('.js-user-link'); + beforeEach(() => { + createWrapper({ editedBy }); + }); - expect(authorLink.attributes('href')).toEqual(propsData.editedBy.path); - expect(authorLink.text().trim()).toEqual(propsData.editedBy.name); + it('should render user information', () => { + const authorLink = findUserElement(); + + expect(authorLink.attributes('href')).toEqual(editedBy.path); + expect(authorLink.text().trim()).toEqual(editedBy.name); + }); }); }); diff --git a/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb index 7d4e3c3bcce..450ebe9a719 100644 --- a/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb @@ -2,10 +2,10 @@ require 'spec_helper' -RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do +RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter, feature_category: :importers do subject(:importer) { described_class.new(note_text, project, client) } - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, import_source: 'nickname/public-test-repo') } let(:note_text) { Gitlab::GithubImport::Representation::NoteText.from_db_record(record) } let(:client) { instance_double('Gitlab::GithubImport::Client') } @@ -13,6 +13,8 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do let(:doc_url) { 'https://github.com/nickname/public-test-repo/files/9020437/git-cheat-sheet.txt' } let(:image_url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ef2.jpeg' } let(:image_tag_url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ea5.jpeg' } + let(:project_blob_url) { 'https://github.com/nickname/public-test-repo/blob/main/example.md' } + let(:other_project_blob_url) { 'https://github.com/nickname/other-repo/blob/main/README.md' } let(:text) do <<-TEXT.split("\n").map(&:strip).join("\n") Some text... @@ -20,11 +22,14 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do [special-doc](#{doc_url}) ![image.jpeg](#{image_url}) \"tag-image\" + + [link to project blob file](#{project_blob_url}) + [link to other project blob file](#{other_project_blob_url}) TEXT end shared_examples 'updates record description' do - it do + it 'changes attachment links' do importer.execute record.reload @@ -32,6 +37,22 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter do expect(record.description).to include('![image.jpeg](/uploads/') expect(record.description).to include('tag-image '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