diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-21 00:11:07 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-21 00:11:07 +0000 |
commit | 9f021745a60369298705393103f621ff73a2b286 (patch) | |
tree | a6e9f85a8f35245e4ccef5f532c6ca6857f08d9e | |
parent | ef8c47e97e1c178291e4857314a3f53875d75062 (diff) | |
download | gitlab-ce-9f021745a60369298705393103f621ff73a2b286.tar.gz |
Add latest changes from gitlab-org/gitlab@master
86 files changed, 1349 insertions, 1170 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 38426bae943..15bec7530ee 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,7 +17,7 @@ stages: # in cases where jobs require Docker-in-Docker, the job # definition must be extended with `.use-docker-in-docker` default: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-85-node-12.18-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-12.18-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" tags: - gitlab-org # All jobs are interruptible by default diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 355607c17ac..45bff293cd4 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -71,7 +71,7 @@ policy: pull .use-pg11: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-85-node-12.18-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-12.18-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" services: - name: postgres:11.6 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -80,7 +80,7 @@ POSTGRES_HOST_AUTH_METHOD: trust .use-pg12: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-85-node-12.18-yarn-1.22-postgresql-12-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-12.18-yarn-1.22-postgresql-12-graphicsmagick-1.3.34" services: - name: postgres:12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -89,7 +89,7 @@ POSTGRES_HOST_AUTH_METHOD: trust .use-pg11-ee: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-85-node-12.18-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-12.18-yarn-1.22-postgresql-11-graphicsmagick-1.3.34" services: - name: postgres:11.6 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -100,7 +100,7 @@ POSTGRES_HOST_AUTH_METHOD: trust .use-pg12-ee: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-85-node-12.18-yarn-1.22-postgresql-12-graphicsmagick-1.3.34" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7.2-golang-1.14-git-2.29-lfs-2.9-chrome-87-node-12.18-yarn-1.22-postgresql-12-graphicsmagick-1.3.34" services: - name: postgres:12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -25,7 +25,7 @@ gem 'marginalia', '~> 1.10.0' gem 'devise', '~> 4.7.2' # TODO: verify ARM compile issue on 3.1.13+ version (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18828) gem 'bcrypt', '3.1.12' -gem 'doorkeeper', '~> 5.3.0' +gem 'doorkeeper', '~> 5.4.0' gem 'doorkeeper-openid_connect', '~> 1.7.4' gem 'omniauth', '~> 1.8' gem 'omniauth-auth0', '~> 2.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 74d88417541..974bc4181f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -260,7 +260,7 @@ GEM docile (1.3.2) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - doorkeeper (5.3.3) + doorkeeper (5.4.0) railties (>= 5) doorkeeper-openid_connect (1.7.4) doorkeeper (>= 5.2, < 5.5) @@ -1325,7 +1325,7 @@ DEPENDENCIES diff_match_patch (~> 0.1.0) diffy (~> 3.3) discordrb-webhooks-blackst0ne (~> 3.3) - doorkeeper (~> 5.3.0) + doorkeeper (~> 5.4.0) doorkeeper-openid_connect (~> 1.7.4) ed25519 (~> 1.2) elasticsearch-api (~> 6.8.2) diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue index 5402fed0a9a..67110265b5f 100644 --- a/app/assets/javascripts/design_management/components/list/item.vue +++ b/app/assets/javascripts/design_management/components/list/item.vue @@ -133,7 +133,7 @@ export default { <div class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative" > - <div v-if="icon.name" data-testid="design-event" class="design-event gl-absolute"> + <div v-if="icon.name" data-testid="design-event" class="gl-top-5 gl-right-5 gl-absolute"> <span :title="icon.tooltip" :aria-label="icon.tooltip"> <gl-icon :name="icon.name" diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 2c51ce0d970..1a3c15bedb8 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -1,3 +1,4 @@ +import { isEqual } from 'lodash'; import * as utils from './utils'; import * as types from './mutation_types'; import * as constants from '../constants'; @@ -31,7 +32,8 @@ export default { } } - note.base_discussion = undefined; // No point keeping a reference to this + // note.base_discussion = undefined; // No point keeping a reference to this + delete note.base_discussion; discussion.notes = [note]; state.discussions.push(discussion); @@ -220,6 +222,11 @@ export default { [types.UPDATE_NOTE](state, note) { const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id); + // Disable eslint here so we can delete the property that we no longer need + // in the note object + // eslint-disable-next-line no-param-reassign + delete note.base_discussion; + if (noteObj.individual_note) { if (note.type === constants.DISCUSSION_NOTE) { noteObj.individual_note = false; @@ -228,7 +235,10 @@ export default { noteObj.notes.splice(0, 1, note); } else { const comment = utils.findNoteObjectById(noteObj.notes, note.id); - noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); + + if (!isEqual(comment, note)) { + noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note); + } } }, diff --git a/app/assets/javascripts/search/topbar/components/project_filter.vue b/app/assets/javascripts/search/topbar/components/project_filter.vue index 3f1f3848ac7..effaafb72c1 100644 --- a/app/assets/javascripts/search/topbar/components/project_filter.vue +++ b/app/assets/javascripts/search/topbar/components/project_filter.vue @@ -27,7 +27,7 @@ export default { handleProjectChange(project) { // This determines if we need to update the group filter or not const queryParams = { - ...(project.namespace_id && { [GROUP_DATA.queryParam]: project.namespace_id }), + ...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }), [PROJECT_DATA.queryParam]: project.id, }; diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 5ee51764555..94a216596a8 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -64,6 +64,11 @@ export default { mounted() { this.renderSuggestions(); }, + beforeDestroy() { + if (this.suggestionsWatch) { + this.suggestionsWatch(); + } + }, methods: { renderSuggestions() { // swaps out suggestion(s) markdown with rich diff components @@ -108,6 +113,13 @@ export default { }, }); + // We're using `$watch` as `suggestionsCount` updates do not + // propagate to this component for some unknown reason while + // using a traditional prop watcher. + this.suggestionsWatch = this.$watch('suggestionsCount', () => { + suggestionDiff.suggestionsCount = this.suggestionsCount; + }); + suggestionDiff.$on('apply', ({ suggestionId, callback, message }) => { this.$emit('apply', { suggestionId, callback, flashContainer: this.$el, message }); }); diff --git a/app/assets/stylesheets/components/design_management/design_list_item.scss b/app/assets/stylesheets/components/design_management/design_list_item.scss index b7f6b2026fe..09af4da37e9 100644 --- a/app/assets/stylesheets/components/design_management/design_list_item.scss +++ b/app/assets/stylesheets/components/design_management/design_list_item.scss @@ -8,11 +8,6 @@ top: 10px; } - .design-event { - top: $gl-padding; - right: $gl-padding; - } - .card-body { height: 230px; } diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb index baebedb8e5d..a3ea39d9c3d 100644 --- a/app/controllers/concerns/integrations_actions.rb +++ b/app/controllers/concerns/integrations_actions.rb @@ -34,10 +34,6 @@ module IntegrationsActions end end - def custom_integration_projects - Project.with_custom_integration_compared_to(integration).page(params[:page]).per(20) - end - def test render json: {}, status: :ok end diff --git a/app/finders/terraform/states_finder.rb b/app/finders/terraform/states_finder.rb new file mode 100644 index 00000000000..bbe90fead2b --- /dev/null +++ b/app/finders/terraform/states_finder.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Terraform + class StatesFinder + def initialize(project, current_user, params: {}) + @project = project + @current_user = current_user + @params = params + end + + def execute + return ::Terraform::State.none unless can_read_terraform_states? + + states = project.terraform_states + states = states.with_name(params[:name]) if params[:name].present? + + states.ordered_by_name + end + + private + + attr_reader :project, :current_user, :params + + def can_read_terraform_states? + current_user.can?(:read_terraform_state, project) + end + end +end diff --git a/app/graphql/mutations/security/ci_configuration/configure_sast.rb b/app/graphql/mutations/security/ci_configuration/configure_sast.rb new file mode 100644 index 00000000000..6cb3704a19b --- /dev/null +++ b/app/graphql/mutations/security/ci_configuration/configure_sast.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Mutations + module Security + module CiConfiguration + class ConfigureSast < BaseMutation + include ResolvesProject + + graphql_name 'ConfigureSast' + + argument :project_path, GraphQL::ID_TYPE, + required: true, + description: 'Full path of the project.' + + argument :configuration, ::Types::CiConfiguration::Sast::InputType, + required: true, + description: 'SAST CI configuration for the project.' + + field :status, GraphQL::STRING_TYPE, null: false, + description: 'Status of creating the commit for the supplied SAST CI configuration.' + + field :success_path, GraphQL::STRING_TYPE, null: true, + description: 'Redirect path to use when the response is successful.' + + authorize :push_code + + def resolve(project_path:, configuration:) + project = authorized_find!(full_path: project_path) + + result = ::Security::CiConfiguration::SastCreateService.new(project, current_user, configuration).execute + prepare_response(result) + end + + private + + def find_object(full_path:) + resolve_project(full_path: full_path) + end + + def prepare_response(result) + { + status: result[:status], + success_path: result[:success_path], + errors: Array(result[:errors]) + } + end + end + end + end +end diff --git a/app/graphql/resolvers/terraform/states_resolver.rb b/app/graphql/resolvers/terraform/states_resolver.rb index 38b26a948b1..f543eb182e8 100644 --- a/app/graphql/resolvers/terraform/states_resolver.rb +++ b/app/graphql/resolvers/terraform/states_resolver.rb @@ -3,20 +3,20 @@ module Resolvers module Terraform class StatesResolver < BaseResolver - type Types::Terraform::StateType, null: true + type Types::Terraform::StateType.connection_type, null: true alias_method :project, :object - def resolve(**args) - return ::Terraform::State.none unless can_read_terraform_states? - - project.terraform_states.ordered_by_name + when_single do + argument :name, GraphQL::STRING_TYPE, + required: true, + description: 'Name of the Terraform state.' end - private - - def can_read_terraform_states? - current_user.can?(:read_terraform_state, project) + def resolve(**args) + ::Terraform::StatesFinder + .new(project, current_user, params: args) + .execute end end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index f9dd11cbe37..11980e009f4 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -15,6 +15,7 @@ module Types mount_mutation Mutations::AlertManagement::HttpIntegration::Update mount_mutation Mutations::AlertManagement::HttpIntegration::ResetToken mount_mutation Mutations::AlertManagement::HttpIntegration::Destroy + mount_mutation Mutations::Security::CiConfiguration::ConfigureSast mount_mutation Mutations::AlertManagement::PrometheusIntegration::Create mount_mutation Mutations::AlertManagement::PrometheusIntegration::Update mount_mutation Mutations::AlertManagement::PrometheusIntegration::ResetToken diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 41a4edd20a5..257c77e2ede 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -309,10 +309,16 @@ module Types description: 'Title of the label' end + field :terraform_state, + Types::Terraform::StateType, + null: true, + description: 'Find a single Terraform state by name.', + resolver: Resolvers::Terraform::StatesResolver.single + field :terraform_states, Types::Terraform::StateType.connection_type, null: true, - description: 'Terraform states associated with the project', + description: 'Terraform states associated with the project.', resolver: Resolvers::Terraform::StatesResolver field :pipeline_analytics, Types::Ci::AnalyticsType, null: true, diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 133d9d21a14..eeeffb7b3ae 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -62,6 +62,14 @@ module GroupsHelper can?(current_user, :set_emails_disabled, group) && !group.parent&.emails_disabled? end + def group_open_issues_count(group) + if Feature.enabled?(:cached_sidebar_open_issues_count, group) + cached_open_group_issues_count(group) + else + number_with_delimiter(group_issues_count(state: 'opened')) + end + end + def group_issues_count(state:) IssuesFinder .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) @@ -69,6 +77,21 @@ module GroupsHelper .count end + def cached_open_group_issues_count(group) + count_service = Groups::OpenIssuesCountService + issues_count = count_service.new(group, current_user).count + + if issues_count > count_service::CACHED_COUNT_THRESHOLD + ActiveSupport::NumberHelper + .number_to_human( + issues_count, + units: { thousand: 'k', million: 'm' }, precision: 1, significant: false, format: '%n%u' + ) + else + number_with_delimiter(issues_count) + end + end + def group_merge_requests_count(state:) MergeRequestsFinder .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) diff --git a/app/models/terraform/state.rb b/app/models/terraform/state.rb index efbbd86ae4a..a301c4a9689 100644 --- a/app/models/terraform/state.rb +++ b/app/models/terraform/state.rb @@ -22,6 +22,7 @@ module Terraform scope :versioning_not_enabled, -> { where(versioning_enabled: false) } scope :ordered_by_name, -> { order(:name) } + scope :with_name, -> (name) { where(name: name) } validates :project_id, presence: true validates :uuid, presence: true, uniqueness: true, length: { is: UUID_LENGTH }, diff --git a/app/services/groups/open_issues_count_service.rb b/app/services/groups/open_issues_count_service.rb new file mode 100644 index 00000000000..db1ca09212a --- /dev/null +++ b/app/services/groups/open_issues_count_service.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Groups + # Service class for counting and caching the number of open issues of a group. + class OpenIssuesCountService < BaseCountService + include Gitlab::Utils::StrongMemoize + + VERSION = 1 + PUBLIC_COUNT_KEY = 'group_public_open_issues_count' + TOTAL_COUNT_KEY = 'group_total_open_issues_count' + CACHED_COUNT_THRESHOLD = 1000 + EXPIRATION_TIME = 24.hours + + attr_reader :group, :user + + def initialize(group, user = nil) + @group = group + @user = user + end + + # Reads count value from cache and return it if present. + # If empty or expired, #uncached_count will calculate the issues count for the group and + # compare it with the threshold. If it is greater, it will be written to the cache and returned. + # If below, it will be returned without being cached. + # This results in only caching large counts and calculating the rest with every call to maintain + # accuracy. + def count + cached_count = Rails.cache.read(cache_key) + return cached_count unless cached_count.blank? + + refreshed_count = uncached_count + update_cache_for_key(cache_key) { refreshed_count } if refreshed_count > CACHED_COUNT_THRESHOLD + refreshed_count + end + + def cache_key(key = nil) + ['groups', 'open_issues_count_service', VERSION, group.id, cache_key_name] + end + + private + + def cache_options + super.merge({ expires_in: EXPIRATION_TIME }) + end + + def cache_key_name + public_only? ? PUBLIC_COUNT_KEY : TOTAL_COUNT_KEY + end + + def public_only? + !user_is_at_least_reporter? + end + + def user_is_at_least_reporter? + strong_memoize(:user_is_at_least_reporter) do + group.member?(user, Gitlab::Access::REPORTER) + end + end + + def relation_for_count + IssuesFinder.new(user, group_id: group.id, state: 'opened', non_archived: true, include_subgroups: true, public_only: public_only?).execute + end + end +end diff --git a/app/services/security/ci_configuration/sast_create_service.rb b/app/services/security/ci_configuration/sast_create_service.rb new file mode 100644 index 00000000000..8fc3b8d078c --- /dev/null +++ b/app/services/security/ci_configuration/sast_create_service.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module Security + module CiConfiguration + class SastCreateService < ::BaseService + def initialize(project, current_user, params) + @project = project + @current_user = current_user + @params = params + @branch_name = @project.repository.next_branch('set-sast-config') + end + + def execute + attributes_for_commit = attributes + result = ::Files::MultiService.new(@project, @current_user, attributes_for_commit).execute + + if result[:status] == :success + result[:success_path] = successful_change_path + track_event(attributes_for_commit) + else + result[:errors] = result[:message] + end + + result + + rescue Gitlab::Git::PreReceiveError => e + { status: :error, errors: e.message } + end + + private + + def attributes + actions = Security::CiConfiguration::SastBuildActions.new(@project.auto_devops_enabled?, @params, existing_gitlab_ci_content).generate + + @project.repository.add_branch(@current_user, @branch_name, @project.default_branch) + message = _('Set .gitlab-ci.yml to enable or configure SAST') + + { + commit_message: message, + branch_name: @branch_name, + start_branch: @branch_name, + actions: actions + } + end + + def existing_gitlab_ci_content + gitlab_ci_yml = @project.repository.gitlab_ci_yml_for(@project.repository.root_ref_sha) + YAML.safe_load(gitlab_ci_yml) if gitlab_ci_yml + end + + def successful_change_path + description = _('Set .gitlab-ci.yml to enable or configure SAST security scanning using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST settings.') + merge_request_params = { source_branch: @branch_name, description: description } + Gitlab::Routing.url_helpers.project_new_merge_request_url(@project, merge_request: merge_request_params) + end + + def track_event(attributes_for_commit) + action = attributes_for_commit[:actions].first + + Gitlab::Tracking.event( + self.class.to_s, action[:action], label: action[:default_values_overwritten].to_s + ) + end + end + end +end diff --git a/app/views/groups/_import_group_from_another_instance_panel.html.haml b/app/views/groups/_import_group_from_another_instance_panel.html.haml index c95e7c16161..83d2e13d345 100644 --- a/app/views/groups/_import_group_from_another_instance_panel.html.haml +++ b/app/views/groups/_import_group_from_another_instance_panel.html.haml @@ -2,9 +2,18 @@ = form_errors(@group) .gl-border-l-solid.gl-border-r-solid.gl-border-gray-100.gl-border-1.gl-p-5 - %h4 + %h4.gl-display-flex = s_('GroupsNew|Import groups from another instance of GitLab') - %p + %span.badge.badge-info.badge-pill.gl-badge.md.gl-ml-3 + = _('Beta') + .gl-alert.gl-alert-warning{ role: 'alert' } + = sprite_icon('warning', css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title') + .gl-alert-body + - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') } + - feedback_link_start = '<a href="https://gitlab.com/gitlab-org/gitlab/-/issues/284495" target="_blank" rel="noopener noreferrer">'.html_safe + - link_end = '</a>'.html_safe + = s_('GroupsNew|Not all related objects are migrated, as %{docs_link_start}described here%{docs_link_end}. Please %{feedback_link_start}leave feedback%{feedback_link_end} on this feature.').html_safe % { docs_link_start: docs_link_start, docs_link_end: link_end, feedback_link_start: feedback_link_start, feedback_link_end: link_end } + %p.gl-mt-3 = s_('GroupsNew|Provide credentials for another instance of GitLab to import your groups directly.') .form-group.gl-display-flex.gl-flex-direction-column = f.label :bulk_import_gitlab_url, s_('GroupsNew|GitLab source URL'), for: 'import_gitlab_url' diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml index 3fc50cc86d2..80739395713 100644 --- a/app/views/groups/runners/_runner.html.haml +++ b/app/views/groups/runners/_runner.html.haml @@ -77,8 +77,9 @@ = link_to resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-default has-tooltip', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do = sprite_icon('play') - if runner.belongs_to_more_than_one_project? - .btn-group - .btn.btn-danger.has-tooltip{ 'aria-label' => 'Remove', 'data-container' => 'body', 'data-original-title' => _('Multi-project Runners cannot be removed'), 'data-placement' => 'top', disabled: 'disabled' } + - delete_runner_tooltip = _('Multi-project Runners cannot be removed') + .btn-group.has-tooltip{ data: { container: 'body', placement: 'top' }, title: delete_runner_tooltip } + .btn.btn-danger{ 'aria-label' => delete_runner_tooltip, disabled: 'disabled' } = sprite_icon('close') - else .btn-group diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 5569a69222f..52d3c8d133a 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1,4 +1,4 @@ -- issues_count = group_issues_count(state: 'opened') +- issues_count = group_open_issues_count(@group) - merge_requests_count = group_merge_requests_count(state: 'opened') .nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **tracking_attrs('groups_side_navigation', 'render', 'groups_side_navigation') } @@ -54,14 +54,14 @@ = sprite_icon('issues') %span.nav-item-name = _('Issues') - %span.badge.badge-pill.count= number_with_delimiter(issues_count) + %span.badge.badge-pill.count= issues_count %ul.sidebar-sub-level-items{ data: { qa_selector: 'group_issues_sidebar_submenu'} } = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index', 'iterations#index'], html_options: { class: "fly-out-top-item" } ) do = link_to issues_group_path(@group) do %strong.fly-out-top-item-name = _('Issues') - %span.badge.badge-pill.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count) + %span.badge.badge-pill.count.issue_counter.fly-out-badge= issues_count %li.divider.fly-out-top-item = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index dc4172e2f09..9ea4d3df631 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -50,10 +50,10 @@ - if can?(current_user, :push_code, @project) - if branch.name == @project.repository.root_ref - %button{ class: "gl-button btn btn-danger remove-row has-tooltip disabled", - disabled: true, - title: s_('Branches|The default branch cannot be deleted') } - = sprite_icon("remove") + - delete_default_branch_tooltip = s_('Branches|The default branch cannot be deleted') + %span.has-tooltip{ title: delete_default_branch_tooltip } + %button{ class: "gl-button btn btn-danger remove-row disabled", disabled: true, 'aria-label' => delete_default_branch_tooltip } + = sprite_icon("remove") - elsif protected_branch?(@project, branch) - if can?(current_user, :push_to_delete_protected_branch, @project) %button{ class: "gl-button btn btn-danger remove-row has-tooltip", @@ -65,10 +65,10 @@ is_merged: ("true" if merged) } } = sprite_icon("remove") - else - %button{ class: "gl-button btn btn-danger remove-row has-tooltip disabled", - disabled: true, - title: s_('Branches|Only a project maintainer or owner can delete a protected branch') } - = sprite_icon("remove") + - delete_protected_branch_tooltip = s_('Branches|Only a project maintainer or owner can delete a protected branch') + %span.has-tooltip{ title: delete_protected_branch_tooltip } + %button{ class: "gl-button btn btn-danger remove-row disabled", disabled: true, 'aria-label' => delete_protected_branch_tooltip } + = sprite_icon("remove") - else = link_to project_branch_path(@project, branch.name), class: "gl-button btn btn-danger remove-row qa-remove-btn js-ajax-loading-spinner has-tooltip", diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index dbe0bf35b98..fad168da71e 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -7,11 +7,11 @@ %span= s_('ProjectOverview|Fork') - else - can_create_fork = current_user.can?(:create_fork) - = link_to new_project_fork_path(@project), - class: "btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}", - title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do - = sprite_icon('fork', css_class: 'icon') - %span= s_('ProjectOverview|Fork') + - disabled_fork_tooltip = s_('ProjectOverview|You have reached your project limit') + %span.has-tooltip{ title: (disabled_fork_tooltip unless can_create_fork) } + = link_to new_project_fork_path(@project), class: "btn btn-default btn-xs count-badge-button d-flex align-items-center fork-btn #{' disabled' unless can_create_fork }", 'aria-label' => (disabled_fork_tooltip unless can_create_fork) do + = sprite_icon('fork', css_class: 'icon') + %span= s_('ProjectOverview|Fork') %span.fork-count.count-badge-count.d-flex.align-items-center = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do = @project.forks_count diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index a785e36fad5..d11b61466e2 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -39,15 +39,15 @@ - if can?(current_user, :award_emoji, note) - if note.emoji_awardable? .note-actions-item - = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do - %span{ class: 'link-highlight award-control-icon-neutral' }= sprite_icon('slight-smile') - %span{ class: 'link-highlight award-control-icon-positive' }= sprite_icon('smiley') - %span{ class: 'link-highlight award-control-icon-super-positive' }= sprite_icon('smile') + = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip btn gl-button btn-icon btn-default-tertiary btn-transparent", data: { position: 'right', container: 'body' } do + = sprite_icon('slight-smile', css_class: 'link-highlight award-control-icon-neutral gl-button-icon gl-icon gl-text-gray-400') + = sprite_icon('smiley', css_class: 'link-highlight award-control-icon-positive gl-button-icon gl-icon gl-left-3!') + = sprite_icon('smile', css_class: 'link-highlight award-control-icon-super-positive gl-button-icon gl-icon gl-left-3! ') - if note_editable - .note-actions-item - = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body', qa_selector: 'edit_comment_button' } do + .note-actions-item.gl-ml-0 + = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn gl-button btn-default-tertiary btn-transparent gl-px-2!', data: { container: 'body', qa_selector: 'edit_comment_button' } do %span.link-highlight - = custom_icon('icon_pencil') + = sprite_icon('pencil', css_class: 'gl-button-icon gl-icon gl-text-gray-400 s16') = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index 8cf1b6b9294..c81a3683e90 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -1,10 +1,9 @@ - is_current_user = current_user == note.author - if note_editable || !is_current_user - .dropdown.more-actions.note-actions-item - = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body', qa_selector: 'more_actions_dropdown' } do - %span.icon - = custom_icon('ellipsis_v') + %div{ class: "dropdown more-actions note-actions-item gl-ml-0!" } + = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn gl-button btn-default-tertiary btn-transparent gl-pl-2! gl-pr-0!', data: { toggle: 'dropdown', container: 'body', qa_selector: 'more_actions_dropdown' } do + = sprite_icon('ellipsis_v', css_class: 'gl-button-icon gl-icon gl-text-gray-400') %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %li = clipboard_button(text: noteable_note_url(note), title: _('Copy reference'), button_text: _('Copy link'), class: 'btn-clipboard', hide_tooltip: true, hide_button_icon: true) diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index e9ace8c72f1..ec3fc27dc20 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -4,7 +4,8 @@ = s_('GitLabPages|Configure pages') .card-body %p.gl-mb-0 - - link_start = "<a href='#{help_page_path('user/project/pages/index.md')}' target='_blank' rel='noopener noreferrer'>".html_safe + - docs_link_start = "<a href='#{help_page_path('user/project/pages/index.md')}' target='_blank' rel='noopener noreferrer'>".html_safe + - samples_link_start = "<a href='https://gitlab.com/pages' target='_blank' rel='noopener noreferrer'>".html_safe + - templates_link_start = "<a href='https://gitlab.com/gitlab-org/project-templates' target='_blank' rel='noopener noreferrer'>".html_safe - link_end = '</a>'.html_safe - = s_('GitLabPages|Learn how to upload your static site and have it served by GitLab by following the %{link_start}documentation on GitLab Pages%{link_end}.').html_safe % { link_start: link_start, - link_end: link_end } + = s_('GitLabPages|See the %{docs_link_start}GitLab Pages documentation%{link_end} to learn how to upload your static site and have GitLab serve it. You can also follow a %{samples_link_start}sample project%{link_end} or use a %{templates_link_start}GitLab CI template%{link_end}.').html_safe % { docs_link_start: docs_link_start, samples_link_start: samples_link_start, templates_link_start: templates_link_start, link_end: link_end } diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml index f206a2152c2..089643f4748 100644 --- a/app/views/shared/access_tokens/_form.html.haml +++ b/app/views/shared/access_tokens/_form.html.haml @@ -30,4 +30,4 @@ = render 'shared/tokens/scopes_form', prefix: prefix, token: token, scopes: scopes .gl-mt-3 - = f.submit _('Create %{type}') % { type: type }, class: 'btn btn-success', data: { qa_selector: 'create_token_button' } + = f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-success', data: { qa_selector: 'create_token_button' } diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml index 50daa400e6c..d7c74255578 100644 --- a/app/views/shared/access_tokens/_table.html.haml +++ b/app/views/shared/access_tokens/_table.html.haml @@ -43,7 +43,7 @@ - else %span.token-never-expires-label= _('Never') %td= token.scopes.present? ? token.scopes.join(', ') : _('no scopes selected') - %td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: 'btn btn-danger float-right qa-revoke-button', data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type } } + %td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: 'gl-button btn btn-danger float-right qa-revoke-button', data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type } } - else .settings-message.text-center = no_active_tokens_message diff --git a/changelogs/unreleased/231204-projects-notes.yml b/changelogs/unreleased/231204-projects-notes.yml new file mode 100644 index 00000000000..e1952e214a3 --- /dev/null +++ b/changelogs/unreleased/231204-projects-notes.yml @@ -0,0 +1,5 @@ +--- +title: Apply GitLab UI button styles to buttons in app/views/projects/notes directory +merge_request: 44107 +author: Lakshit +type: other diff --git a/changelogs/unreleased/290231-warning-message-for-group-migration.yml b/changelogs/unreleased/290231-warning-message-for-group-migration.yml new file mode 100644 index 00000000000..bb51802e835 --- /dev/null +++ b/changelogs/unreleased/290231-warning-message-for-group-migration.yml @@ -0,0 +1,5 @@ +--- +title: Add warning message for GitLab group migration +merge_request: 51214 +author: +type: changed diff --git a/changelogs/unreleased/291140-query-single-state.yml b/changelogs/unreleased/291140-query-single-state.yml new file mode 100644 index 00000000000..ba7e586a493 --- /dev/null +++ b/changelogs/unreleased/291140-query-single-state.yml @@ -0,0 +1,5 @@ +--- +title: Add GraphQL query for single Terraform state +merge_request: 51145 +author: +type: added diff --git a/changelogs/unreleased/293477-update-pages-help-text.yml b/changelogs/unreleased/293477-update-pages-help-text.yml new file mode 100644 index 00000000000..5117aec4fcc --- /dev/null +++ b/changelogs/unreleased/293477-update-pages-help-text.yml @@ -0,0 +1,5 @@ +--- +title: Update links in Pages settings +merge_request: 51847 +author: +type: other diff --git a/changelogs/unreleased/299213-project-filter-regression.yml b/changelogs/unreleased/299213-project-filter-regression.yml new file mode 100644 index 00000000000..243b408ebf8 --- /dev/null +++ b/changelogs/unreleased/299213-project-filter-regression.yml @@ -0,0 +1,5 @@ +--- +title: Global Search - Project Filter sets Group +merge_request: 52015 +author: +type: fixed diff --git a/changelogs/unreleased/id-bump-doorkeeper-to-5-4-0.yml b/changelogs/unreleased/id-bump-doorkeeper-to-5-4-0.yml new file mode 100644 index 00000000000..46790583400 --- /dev/null +++ b/changelogs/unreleased/id-bump-doorkeeper-to-5-4-0.yml @@ -0,0 +1,5 @@ +--- +title: Bump doorkeeper to 5.4.0 +merge_request: 51559 +author: +type: other diff --git a/changelogs/unreleased/move_mutations_to_ce_for_sast_config.yml b/changelogs/unreleased/move_mutations_to_ce_for_sast_config.yml new file mode 100644 index 00000000000..edaf88df906 --- /dev/null +++ b/changelogs/unreleased/move_mutations_to_ce_for_sast_config.yml @@ -0,0 +1,5 @@ +--- +title: 'Move to CE: mutation to create MR for SAST Configuration' +merge_request: 51634 +author: +type: changed diff --git a/changelogs/unreleased/ph-235741-fixSuggestionsUpdatingIncorrectly.yml b/changelogs/unreleased/ph-235741-fixSuggestionsUpdatingIncorrectly.yml new file mode 100644 index 00000000000..2b7bbef0223 --- /dev/null +++ b/changelogs/unreleased/ph-235741-fixSuggestionsUpdatingIncorrectly.yml @@ -0,0 +1,5 @@ +--- +title: Fixed notes polling incorrectly overwriting suggestions in the DOM +merge_request: 51988 +author: +type: fixed diff --git a/changelogs/unreleased/yo-gl-button-revoke-button.yml b/changelogs/unreleased/yo-gl-button-revoke-button.yml new file mode 100644 index 00000000000..9d5e8c61e54 --- /dev/null +++ b/changelogs/unreleased/yo-gl-button-revoke-button.yml @@ -0,0 +1,5 @@ +--- +title: Add gl-button to personal access token page +merge_request: 51294 +author: Yogi (@yo) +type: other diff --git a/config/feature_flags/development/cached_sidebar_open_issues_count.yml b/config/feature_flags/development/cached_sidebar_open_issues_count.yml new file mode 100644 index 00000000000..e94566057fc --- /dev/null +++ b/config/feature_flags/development/cached_sidebar_open_issues_count.yml @@ -0,0 +1,8 @@ +--- +name: cached_sidebar_open_issues_count +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49739 +rollout_issue_url: +milestone: '13.8' +type: development +group: group::product planning +default_enabled: false diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 64dd074a912..ac24ab0bc49 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -19316,7 +19316,17 @@ type Project { tagList: String """ - Terraform states associated with the project + Find a single Terraform state by name. + """ + terraformState( + """ + Name of the Terraform state. + """ + name: String! + ): TerraformState + + """ + Terraform states associated with the project. """ terraformStates( """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 790c641a6c0..3c751925558 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -56123,8 +56123,35 @@ "deprecationReason": null }, { + "name": "terraformState", + "description": "Find a single Terraform state by name.", + "args": [ + { + "name": "name", + "description": "Name of the Terraform state.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "TerraformState", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "terraformStates", - "description": "Terraform states associated with the project", + "description": "Terraform states associated with the project.", "args": [ { "name": "after", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 31e9e46b6be..1497e8c2aca 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2787,7 +2787,8 @@ Autogenerated return type of PipelineRetry. | `statistics` | ProjectStatistics | Statistics of the project | | `suggestionCommitMessage` | String | The commit message used to apply merge request suggestions | | `tagList` | String | List of project topics (not Git tags) | -| `terraformStates` | TerraformStateConnection | Terraform states associated with the project | +| `terraformState` | TerraformState | Find a single Terraform state by name. | +| `terraformStates` | TerraformStateConnection | Terraform states associated with the project. | | `userPermissions` | ProjectPermissions! | Permissions for the current user on the resource | | `visibility` | String | Visibility of the project | | `vulnerabilities` | VulnerabilityConnection | Vulnerabilities reported on the project | diff --git a/doc/api/groups.md b/doc/api/groups.md index eb255f8de00..958e876ba01 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -941,6 +941,19 @@ The `shared_runners_setting` attribute determines whether shared runners are ena | `disabled_with_override` | Disables shared runners for all projects and subgroups in this group, but allows subgroups to override this setting. | | `disabled_and_unoverridable` | Disables shared runners for all projects and subgroups in this group, and prevents subgroups from overriding this setting. | +### Upload a group avatar + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) in GitLab 12.9. + +To upload an avatar file from your file system, use the `--form` argument. This causes +curl to post data using the header `Content-Type: multipart/form-data`. The +`file=` parameter must point to a file on your file system and be preceded by +`@`. For example: + +```shell +curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22" --form "avatar=@/tmp/example.png" +``` + ## Remove group Only available to group owners and administrators. diff --git a/doc/ci/README.md b/doc/ci/README.md index 740be7d1dbd..391d32edfa7 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -160,8 +160,6 @@ Its feature set is listed on the table below according to DevOps stages. Find example project code and tutorials for using GitLab CI/CD with a variety of app frameworks, languages, and platforms on the [CI Examples](examples/README.md) page. -GitLab also provides [example projects](https://gitlab.com/gitlab-examples) pre-configured to use GitLab CI/CD. - ## Administration **(CORE ONLY)** As a GitLab administrator, you can change the default behavior diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 9fa0bb080ac..3384f76b686 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -23,31 +23,35 @@ Examples are available in several forms. As a collection of: The following table lists examples with step-by-step tutorials that are contained in this section: | Use case | Resource | -|:------------------------------|:---------| +|-------------------------------|----------| | Browser performance testing | [Browser Performance Testing with the Sitespeed.io container](../../user/project/merge_requests/browser_performance_testing.md). | | Clojure | [Test a Clojure application with GitLab CI/CD](test-clojure-application.md). | | Deployment with Dpl | [Using `dpl` as deployment tool](deployment/README.md). | | GitLab Pages | See the [GitLab Pages](../../user/project/pages/index.md) documentation for a complete example of deploying a static site. | | End-to-end testing | [End-to-end testing with GitLab CI/CD and WebdriverIO](end_to_end_testing_webdriverio/index.md). | -| Game development | [DevOps and Game Dev with GitLab CI/CD](devops_and_game_dev_with_gitlab_ci_cd/index.md). | -| Java with Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](artifactory_and_gitlab/index.md). | -| Java with Spring Boot | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md). | | Load performance testing | [Load Performance Testing with the k6 container](../../user/project/merge_requests/load_performance_testing.md). | | Multi project pipeline | [Build, test deploy using multi project pipeline](https://gitlab.com/gitlab-examples/upstream-project). | | NPM with semantic-release | [Publish NPM packages to the GitLab Package Registry using semantic-release](semantic-release.md). | | PHP with Laravel, Envoy | [Test and deploy Laravel applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md). | | PHP with NPM, SCP | [Running Composer and NPM scripts with deployment via SCP in GitLab CI/CD](deployment/composer-npm-deploy.md). | | PHP with PHPunit, atoum | [Testing PHP projects](php.md). | -| Parallel testing Ruby & JS | [GitLab CI/CD parallel jobs testing for Ruby & JavaScript projects](https://docs.knapsackpro.com/2019/how-to-run-parallel-jobs-for-rspec-tests-on-gitlab-ci-pipeline-and-speed-up-ruby-javascript-testing). | | Python on Heroku | [Test and deploy a Python application with GitLab CI/CD](test-and-deploy-python-application-to-heroku.md). | | Ruby on Heroku | [Test and deploy a Ruby application with GitLab CI/CD](test-and-deploy-ruby-application-to-heroku.md). | -| Scala on Heroku | [Test and deploy a Scala application to Heroku](test-scala-application.md). | | Secrets management with Vault | [Authenticating and Reading Secrets With Hashicorp Vault](authenticating-with-hashicorp-vault/index.md). | -### How to contributing examples +### Contributed examples + +You can help people that use your favorite programming language by submitting a link +to a guide for that language. These contributed guides are hosted externally or in +separate example projects: -Contributions are welcome! You can help your favorite programming -language users and GitLab by sending a merge request with a guide for that language. +| Use case | Resource | +|-------------------------------|----------| +| Game development | [DevOps and Game Dev with GitLab CI/CD](https://gitlab.com/gitlab-examples/gitlab-game-demo/). | +| Java with Maven | [How to deploy Maven projects to Artifactory with GitLab CI/CD](https://gitlab.com/gitlab-examples/maven/simple-maven-example). | +| Java with Spring Boot | [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](https://gitlab.com/gitlab-examples/spring-gitlab-cf-deploy-demo). | +| Parallel testing Ruby & JS | [GitLab CI/CD parallel jobs testing for Ruby & JavaScript projects](https://docs.knapsackpro.com/2019/how-to-run-parallel-jobs-for-rspec-tests-on-gitlab-ci-pipeline-and-speed-up-ruby-javascript-testing). | +| Scala on Heroku | [Test and deploy a Scala application to Heroku](https://gitlab.com/gitlab-examples/scala-sbt). | ## CI/CD templates diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md index c1df21297e3..a1a7de26cf2 100644 --- a/doc/ci/examples/artifactory_and_gitlab/index.md +++ b/doc/ci/examples/artifactory_and_gitlab/index.md @@ -1,289 +1,8 @@ --- -stage: Verify -group: Continuous Integration -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments -disqus_identifier: 'https://docs.gitlab.com/ee/articles/artifactory_and_gitlab/index.html' -author: Fabio Busatto -author_gitlab: bikebilly -type: tutorial -date: 2017-08-15 +redirect_to: '../README.md#contributed-examples' --- -<!-- vale off --> +This document was moved to [another location](../README.md#contributed-examples). -# How to deploy Maven projects to Artifactory with GitLab CI/CD - -## Introduction - -In this article, we show how you can leverage the power of [GitLab CI/CD](https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/) -to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory](https://jfrog.com/artifactory/), and then use it from another Maven application as a dependency. - -You'll create two different projects: - -- `simple-maven-dep`: the app built and deployed to Artifactory (see the [simple-maven-dep](https://gitlab.com/gitlab-examples/maven/simple-maven-dep) example project) -- `simple-maven-app`: the app using the previous one as a dependency (see the [simple-maven-app](https://gitlab.com/gitlab-examples/maven/simple-maven-app) example project) - -We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/). -We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it. - -## Create the simple Maven dependency - -First, you need an application to work with: in this specific case we'll use a -simple one, but it could be any Maven application. This will be the dependency -you want to package and deploy to Artifactory, to be available to other -projects. - -### Prepare the dependency application - -For this article you'll use a Maven app that can be cloned from our example -project: - -1. Log in to your GitLab account -1. Create a new project by selecting **Import project from > Repo by URL** -1. Add the following URL: - - ```plaintext - https://gitlab.com/gitlab-examples/maven/simple-maven-dep.git - ``` - -1. Click **Create project** - -This application is nothing more than a basic class with a stub for a JUnit based test suite. -It exposes a method called `hello` that accepts a string as input, and prints a hello message on the screen. - -The project structure is really simple, and you should consider these two resources: - -- `pom.xml`: project object model (POM) configuration file -- `src/main/java/com/example/dep/Dep.java`: source of our application - -### Configure the Artifactory deployment - -The application is ready to use, but you need some additional steps to deploy it to Artifactory: - -1. Log in to Artifactory with your user's credentials. -1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel. -1. Copy to clipboard the configuration snippet under the **Deploy** paragraph. -1. Change the `url` value to have it configurable by using variables. -1. Copy the snippet in the `pom.xml` file for your project, just after the - `dependencies` section. The snippet should look like this: - - ```xml - <distributionManagement> - <repository> - <id>central</id> - <name>83d43b5afeb5-releases</name> - <url>${env.MAVEN_REPO_URL}/libs-release-local</url> - </repository> - </distributionManagement> - ``` - -Another step you need to do before you can deploy the dependency to Artifactory -is to configure the authentication data. It is a simple task, but Maven requires -it to stay in a file called `settings.xml` that has to be in the `.m2` subdirectory -in the user's homedir. - -Since you want to use a runner to automatically deploy the application, you -should create the file in the project's home directory and set a command line -parameter in `.gitlab-ci.yml` to use the custom location instead of the default one: - -1. Create a folder called `.m2` in the root of your repository -1. Create a file called `settings.xml` in the `.m2` folder -1. Copy the following content into a `settings.xml` file: - - ```xml - <settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd" - xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - <servers> - <server> - <id>central</id> - <username>${env.MAVEN_REPO_USER}</username> - <password>${env.MAVEN_REPO_PASS}</password> - </server> - </servers> - </settings> - ``` - - Username and password will be replaced by the correct values using variables. - -### Configure GitLab CI/CD for `simple-maven-dep` - -Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/) to automatically build, test and deploy the dependency! - -GitLab CI/CD uses a file in the root of the repository, named `.gitlab-ci.yml`, to read the definitions for jobs -that will be executed by the configured runners. You can read more about this file in the [GitLab Documentation](../../yaml/README.md). - -First of all, remember to set up variables for your deployment. Navigate to your project's **Settings > CI/CD > Environment variables** page -and add the following ones (replace them with your current values, of course): - -- **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) -- **MAVEN_REPO_USER**: `gitlab` (your Artifactory username) -- **MAVEN_REPO_PASS**: `AKCp2WXr3G61Xjz1PLmYa3arm3yfBozPxSta4taP3SeNu2HPXYa7FhNYosnndFNNgoEds8BCS` (your Artifactory Encrypted Password) - -Now it's time to define jobs in `.gitlab-ci.yml` and push it to the repository: - -```yaml -image: maven:latest - -variables: - MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode" - MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" - -cache: - paths: - - .m2/repository/ - - target/ - -build: - stage: build - script: - - mvn $MAVEN_CLI_OPTS compile - -test: - stage: test - script: - - mvn $MAVEN_CLI_OPTS test - -deploy: - stage: deploy - script: - - mvn $MAVEN_CLI_OPTS deploy - only: - - master -``` - -The runner uses the latest [Maven Docker image](https://hub.docker.com/_/maven/), -which contains all of the tools and dependencies needed to manage the project -and to run the jobs. - -Environment variables are set to instruct Maven to use the `homedir` of the repository instead of the user's home when searching for configuration and dependencies. - -Caching the `.m2/repository folder` (where all the Maven files are stored), and the `target` folder (where our application will be created), is useful for speeding up the process -by running all Maven phases in a sequential order, therefore, executing `mvn test` will automatically run `mvn compile` if necessary. - -Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the application. - -Deploy to Artifactory is done as defined by the variables we have just set up. -The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published. - -Done! Now you have all the changes in the GitLab repository, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening. -If the deployment has been successful, the deploy job log will output: - -```plaintext -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 1.983 s -``` - ->**Note**: -the `mvn` command downloads a lot of files from the internet, so you'll see a lot of extra activity in the log the first time you run it. - -Yay! You did it! Checking in Artifactory will confirm that you have a new artifact available in the `libs-release-local` repository. - -## Create the main Maven application - -Now that you have the dependency available on Artifactory, it's time to use it! -Let's see how we can have it as a dependency to our main application. - -### Prepare the main application - -We'll use again a Maven app that can be cloned from our example project: - -1. Create a new project by selecting **Import project from ➔ Repo by URL** -1. Add the following URL: - - ```plaintext - https://gitlab.com/gitlab-examples/maven/simple-maven-app.git - ``` - -1. Click **Create project** - -This one is a simple app as well. If you look at the `src/main/java/com/example/app/App.java` -file you can see that it imports the `com.example.dep.Dep` class and calls the `hello` method passing `GitLab` as a parameter. - -Since Maven doesn't know how to resolve the dependency, you need to modify the configuration: - -1. Go back to Artifactory -1. Browse the `libs-release-local` repository -1. Select the `simple-maven-dep-1.0.jar` file -1. Find the configuration snippet from the **Dependency Declaration** section of the main panel -1. Copy the snippet in the `dependencies` section of the `pom.xml` file. - The snippet should look like this: - - ```xml - <dependency> - <groupId>com.example.dep</groupId> - <artifactId>simple-maven-dep</artifactId> - <version>1.0</version> - </dependency> - ``` - -### Configure the Artifactory repository location - -At this point you defined the dependency for the application, but you still miss where you can find the required files. -You need to create a `.m2/settings.xml` file as you did for the dependency project, and let Maven know the location using environment variables. - -Here is how you can get the content of the file directly from Artifactory: - -1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel -1. Click on **Generate Maven Settings** -1. Click on **Generate Settings** -1. Copy to clipboard the configuration file -1. Save the file as `.m2/settings.xml` in your repository - -Now you are ready to use the Artifactory repository to resolve dependencies and use `simple-maven-dep` in your main application! - -### Configure GitLab CI/CD for `simple-maven-app` - -You need a last step to have everything in place: configure the `.gitlab-ci.yml` file for this project, as you already did for `simple-maven-dep`. - -You want to leverage [GitLab CI/CD](https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/) to automatically build, test and run your awesome application, -and see if you can get the greeting as expected! - -All you need to do is to add the following `.gitlab-ci.yml` to the repository: - -```yaml -image: maven:latest - -stages: - - build - - test - - run - -variables: - MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode" - MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository" - -cache: - paths: - - .m2/repository/ - - target/ - -build: - stage: build - script: - - mvn $MAVEN_CLI_OPTS compile - -test: - stage: test - script: - - mvn $MAVEN_CLI_OPTS test - -run: - stage: run - script: - - mvn $MAVEN_CLI_OPTS package - - mvn $MAVEN_CLI_OPTS exec:java -Dexec.mainClass="com.example.app.App" -``` - -It is very similar to the configuration used for `simple-maven-dep`, but instead of the `deploy` job there is a `run` job. -Probably something that you don't want to use in real projects, but here it is useful to see the application executed automatically. - -And that's it! In the `run` job output log you will find a friendly hello to GitLab! - -## Conclusion - -In this article we covered the basic steps to use an Artifactory Maven repository to automatically publish and consume artifacts. - -A similar approach could be used to interact with any other Maven compatible Binary Repository Manager. -Obviously, you can improve these examples, optimizing the `.gitlab-ci.yml` file to better suit your needs, and adapting to your workflow. +<!-- This redirect file can be deleted after 2021-04-18. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_variables.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_variables.png Binary files differdeleted file mode 100644 index e76767741ce..00000000000 --- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_variables.png +++ /dev/null diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png Binary files differdeleted file mode 100644 index f3761632556..00000000000 --- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png +++ /dev/null diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md index 9c145677f6e..a1a7de26cf2 100644 --- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md @@ -1,145 +1,8 @@ --- -stage: Release -group: Release -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments -author: Dylan Griffith -author_gitlab: DylanGriffith -type: tutorial -date: 2018-06-07 -description: "Continuous Deployment of a Spring Boot application to Cloud Foundry with GitLab CI/CD" +redirect_to: '../README.md#contributed-examples' --- -<!-- vale off --> +This document was moved to [another location](../README.md#contributed-examples). -# Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD - -## Introduction - -This article demonstrates how to use the [Continuous Deployment](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-deployment) -method to deploy a [Spring Boot](https://projects.spring.io/spring-boot/) application to -[Cloud Foundry (CF)](https://www.cloudfoundry.org/) -with GitLab CI/CD. - -All the code for this project can be found in this [GitLab -repository](https://gitlab.com/gitlab-examples/spring-gitlab-cf-deploy-demo). - -In case you're interested in deploying Spring Boot applications to Kubernetes -using GitLab CI/CD, read through the blog post [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/blog/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/). - -## Requirements - -This tutorial assumes you are familiar with Java, GitLab, Cloud Foundry, and GitLab CI/CD. - -To follow along, you need: - -- An account on [Pivotal Web Services (PWS)](https://run.pivotal.io/) or any - other Cloud Foundry (CF) instance. -- An account on GitLab. - -NOTE: -If you're not deploying to PWS, you must replace the `api.run.pivotal.io` URL in all the below -commands with the [API URL](https://docs.cloudfoundry.org/running/cf-api-endpoint.html) -of your CF instance. - -## Create your project - -To create your Spring Boot application you can use the Spring template in -GitLab when creating a new project: - -![New Project From Template](img/create_from_template.png) - -## Configure the deployment to Cloud Foundry - -To deploy to Cloud Foundry you must add a `manifest.yml` file. This -is the configuration for the CF CLI you must use to deploy the application. -Create this in the root directory of your project with the following -content: - -```yaml ---- -applications: - - name: gitlab-hello-world - random-route: true - memory: 1G - path: target/demo-0.0.1-SNAPSHOT.jar -``` - -## Configure GitLab CI/CD to deploy your application - -Now you must add the GitLab CI/CD configuration file -([`.gitlab-ci.yml`](../../yaml/README.md)) -to your project's root. This is how GitLab figures out what commands must run whenever -code is pushed to your repository. Add the following `.gitlab-ci.yml` -file to the root directory of the repository. GitLab detects it -automatically and runs the defined steps once you push your code: - -```yaml -image: java:8 - -stages: - - build - - deploy - -before_script: - - chmod +x mvnw - -build: - stage: build - script: ./mvnw package - artifacts: - paths: - - target/demo-0.0.1-SNAPSHOT.jar - -production: - stage: deploy - script: - - curl --location "https://cli.run.pivotal.io/stable?release=linux64-binary&source=github" | tar zx - - ./cf login -u $CF_USERNAME -p $CF_PASSWORD -a api.run.pivotal.io - - ./cf push - only: - - master -``` - -This uses the `java:8` [Docker image](../../docker/using_docker_images.md) -to build your application, as it provides the up-to-date Java 8 JDK on [Docker Hub](https://hub.docker.com/). -You also added the [`only` clause](../../yaml/README.md#onlyexcept-basic) -to ensure your deployments only happen when you push to the master branch. - -Because the steps defined in `.gitlab-ci.yml` require credentials to sign in to -CF, you must add your CF credentials as -[environment variables](../../variables/README.md#predefined-environment-variables) -in GitLab CI/CD. To set the environment variables, navigate to your project's -**Settings > CI/CD**, and then expand **Variables**. Name the variables -`CF_USERNAME` and `CF_PASSWORD` and set them to the correct values. - -![Variable Settings in GitLab](img/cloud_foundry_variables.png) - -After set up, GitLab CI/CD deploys your app to CF at every push to your -repository's default branch. To review the build logs or watch your builds -running live, navigate to **CI/CD > Pipelines**. - -WARNING: -It's considered best practice for security to create a separate deploy user for -your application and add its credentials to GitLab instead of using a -developer's credentials. - -To start a manual deployment in GitLab go to **CI/CD > Pipelines** then click -**Run Pipeline**. After the app is finished deploying, it displays the -URL of your application in the logs for the `production` job: - -```shell -requested state: started -instances: 1/1 -usage: 1G x 1 instances -urls: gitlab-hello-world-undissembling-hotchpot.cfapps.io -last uploaded: Mon Nov 6 10:02:25 UTC 2017 -stack: cflinuxfs2 -buildpack: client-certificate-mapper=1.2.0_RELEASE container-security-provider=1.8.0_RELEASE java-buildpack=v4.5-offline-https://github.com/cloudfoundry/java-buildpack.git#ffeefb9 java-main java-opts jvmkill-agent=1.10.0_RELEASE open-jdk-like-jre=1.8.0_1... - - state since cpu memory disk details -#0 running 2017-11-06 09:03:22 PM 120.4% 291.9M of 1G 137.6M of 1G -``` - -You can then visit your deployed application (for this example, -`https://gitlab-hello-world-undissembling-hotchpot.cfapps.io/`) and you should -see the "Spring is here!" message. +<!-- This redirect file can be deleted after 2021-04-18. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/img/aws_config_window.png b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/img/aws_config_window.png Binary files differdeleted file mode 100644 index 09eef98202f..00000000000 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/img/aws_config_window.png +++ /dev/null diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/img/gitlab_config.png b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/img/gitlab_config.png Binary files differdeleted file mode 100644 index 71ffcdea289..00000000000 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/img/gitlab_config.png +++ /dev/null diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/img/test_pipeline_pass.png b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/img/test_pipeline_pass.png Binary files differdeleted file mode 100644 index a9452577a42..00000000000 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/img/test_pipeline_pass.png +++ /dev/null diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md index 298ffff568a..0dc412bf002 100644 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md @@ -1,534 +1,8 @@ --- -stage: Verify -group: Continuous Integration -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments -author: Ryan Hall -author_gitlab: blitzgren -type: tutorial -date: 2018-03-07 +redirect_to: '../README.md#ci-cd-examples' --- -<!-- vale off --> +This document was moved to [another location](../README.md#contributed-examples). -# DevOps and Game Dev with GitLab CI/CD - -With advances in WebGL and WebSockets, browsers are extremely viable as game development -platforms without the use of plugins like Adobe Flash. Furthermore, by using GitLab and [AWS](https://aws.amazon.com/), -single game developers, as well as game dev teams, can easily host browser-based games online. - -In this tutorial, we'll focus on DevOps, as well as testing and hosting games with Continuous -Integration/Deployment methods using [GitLab CI/CD](../../README.md). We assume you are familiar with GitLab, JavaScript, -and the basics of game development. - -## The game - -Our [demo game](http://gitlab-game-demo.s3-website-us-east-1.amazonaws.com/) consists of a simple spaceship traveling in space that shoots by clicking the mouse in a given direction. - -Creating a strong CI/CD pipeline at the beginning of developing another game, [Dark Nova](https://www.darknova.io), -was essential for the fast pace the team worked at. This tutorial will build upon my -[previous introductory article](https://ryanhallcs.wordpress.com/2017/03/15/devops-and-game-dev/) and go through the following steps: - -1. Using code from the previous article to start with a bare-bones [Phaser](https://phaser.io) game built by a gulp file -1. Adding and running unit tests -1. Creating a `Weapon` class that can be triggered to spawn a `Bullet` in a given direction -1. Adding a `Player` class that uses this weapon and moves around the screen -1. Adding the sprites we will use for the `Player` and `Weapon` -1. Testing and deploying with Continuous Integration and Continuous Deployment methods - -By the end, we'll have the core of a [playable game](http://gitlab-game-demo.s3-website-us-east-1.amazonaws.com/) -that's tested and deployed on every push to the `master` branch of the [codebase](https://gitlab.com/blitzgren/gitlab-game-demo). -This will also provide -boilerplate code for starting a browser-based game with the following components: - -- Written in [TypeScript](https://www.typescriptlang.org/) and [PhaserJs](https://phaser.io) -- Building, running, and testing with [Gulp](https://gulpjs.com) -- Unit tests with [Chai](https://www.chaijs.com) and [Mocha](https://mochajs.org/) -- CI/CD with GitLab -- Hosting the codebase on GitLab.com -- Hosting the game on AWS -- Deploying to AWS - -## Requirements and setup - -Please refer to my previous article [DevOps and Game Dev](https://ryanhallcs.wordpress.com/2017/03/15/devops-and-game-dev/) to learn the foundational -development tools, running a Hello World-like game, and building this game using GitLab -CI/CD from every new push to master. The `master` branch for this game's [repository](https://gitlab.com/blitzgren/gitlab-game-demo) -contains a completed version with all configurations. If you would like to follow along -with this article, you can clone and work from the `devops-article` branch: - -```shell -git clone git@gitlab.com:blitzgren/gitlab-game-demo.git -git checkout devops-article -``` - -Next, we'll create a small subset of tests that exemplify most of the states I expect -this `Weapon` class to go through. To get started, create a folder called `lib/tests` -and add the following code to a new file `weaponTests.ts`: - -```typescript -import { expect } from 'chai'; -import { Weapon, BulletFactory } from '../lib/weapon'; - -describe('Weapon', () => { - var subject: Weapon; - var shotsFired: number = 0; - // Mocked bullet factory - var bulletFactory: BulletFactory = <BulletFactory>{ - generate: function(px, py, vx, vy, rot) { - shotsFired++; - } - }; - var parent: any = { x: 0, y: 0 }; - - beforeEach(() => { - shotsFired = 0; - subject = new Weapon(bulletFactory, parent, 0.25, 1); - }); - - it('should shoot if not in cooldown', () => { - subject.trigger(true); - subject.update(0.1); - expect(shotsFired).to.equal(1); - }); - - it('should not shoot during cooldown', () => { - subject.trigger(true); - subject.update(0.1); - subject.update(0.1); - expect(shotsFired).to.equal(1); - }); - - it('should shoot after cooldown ends', () => { - subject.trigger(true); - subject.update(0.1); - subject.update(0.3); // longer than timeout - expect(shotsFired).to.equal(2); - }); - - it('should not shoot if not triggered', () => { - subject.update(0.1); - subject.update(0.1); - expect(shotsFired).to.equal(0); - }); -}); -``` - -To build and run these tests using gulp, let's also add the following gulp functions -to the existing `gulpfile.js` file: - -```typescript -gulp.task('build-test', function () { - return gulp.src('src/tests/**/*.ts', { read: false }) - .pipe(tap(function (file) { - // replace file contents with browserify's bundle stream - file.contents = browserify(file.path, { debug: true }) - .plugin(tsify, { project: "./tsconfig.test.json" }) - .bundle(); - })) - .pipe(buffer()) - .pipe(sourcemaps.init({loadMaps: true}) ) - .pipe(gulp.dest('built/tests')); -}); - -gulp.task('run-test', function() { - gulp.src(['./built/tests/**/*.ts']).pipe(mocha()); -}); -``` - -We will start implementing the first part of our game and get these `Weapon` tests to pass. -The `Weapon` class will expose a method to trigger the generation of a bullet at a given -direction and speed. Later we will implement a `Player` class that ties together the user input -to trigger the weapon. In the `src/lib` folder create a `weapon.ts` file. We'll add two classes -to it: `Weapon` and `BulletFactory` which will encapsulate Phaser's **sprite** and -**group** objects, and the logic specific to our game. - -```typescript -export class Weapon { - private isTriggered: boolean = false; - private currentTimer: number = 0; - - constructor(private bulletFactory: BulletFactory, private parent: Phaser.Sprite, private cooldown: number, private bulletSpeed: number) { - } - - public trigger(on: boolean): void { - this.isTriggered = on; - } - - public update(delta: number): void { - this.currentTimer -= delta; - - if (this.isTriggered && this.currentTimer <= 0) { - this.shoot(); - } - } - - private shoot(): void { - // Reset timer - this.currentTimer = this.cooldown; - - // Get velocity direction from player rotation - var parentRotation = this.parent.rotation + Math.PI / 2; - var velx = Math.cos(parentRotation); - var vely = Math.sin(parentRotation); - - // Apply a small forward offset so bullet shoots from head of ship instead of the middle - var posx = this.parent.x - velx * 10 - var posy = this.parent.y - vely * 10; - - this.bulletFactory.generate(posx, posy, -velx * this.bulletSpeed, -vely * this.bulletSpeed, this.parent.rotation); - } -} - -export class BulletFactory { - - constructor(private bullets: Phaser.Group, private poolSize: number) { - // Set all the defaults for this BulletFactory's bullet object - this.bullets.enableBody = true; - this.bullets.physicsBodyType = Phaser.Physics.ARCADE; - this.bullets.createMultiple(30, 'bullet'); - this.bullets.setAll('anchor.x', 0.5); - this.bullets.setAll('anchor.y', 0.5); - this.bullets.setAll('outOfBoundsKill', true); - this.bullets.setAll('checkWorldBounds', true); - } - - public generate(posx: number, posy: number, velx: number, vely: number, rot: number): Phaser.Sprite { - // Pull a bullet from Phaser's Group pool - var bullet = this.bullets.getFirstExists(false); - - // Set the few unique properties about this bullet: rotation, position, and velocity - if (bullet) { - bullet.reset(posx, posy); - bullet.rotation = rot; - bullet.body.velocity.x = velx; - bullet.body.velocity.y = vely; - } - - return bullet; - } -} -``` - -Lastly, we'll redo our entry point, `game.ts`, to tie together both `Player` and `Weapon` objects -as well as add them to the update loop. Here is what the updated `game.ts` file looks like: - -```typescript -import { Player } from "./player"; -import { Weapon, BulletFactory } from "./weapon"; - -window.onload = function() { - var game = new Phaser.Game(800, 600, Phaser.AUTO, 'gameCanvas', { preload: preload, create: create, update: update }); - var player: Player; - var weapon: Weapon; - - // Import all assets prior to loading the game - function preload () { - game.load.image('player', 'assets/player.png'); - game.load.image('bullet', 'assets/bullet.png'); - } - - // Create all entities in the game, after Phaser loads - function create () { - // Create and position the player - var playerSprite = game.add.sprite(400, 550, 'player'); - playerSprite.anchor.setTo(0.5); - player = new Player(game.input, playerSprite, 150); - - var bulletFactory = new BulletFactory(game.add.group(), 30); - weapon = new Weapon(bulletFactory, player.sprite, 0.25, 1000); - - player.loadWeapon(weapon); - } - - // This function is called once every tick, default is 60fps - function update() { - var deltaSeconds = game.time.elapsedMS / 1000; // convert to seconds - player.update(deltaSeconds); - weapon.update(deltaSeconds); - } -} -``` - -Run `gulp serve` and you can run around and shoot. Wonderful! Let's update our CI -pipeline to include running the tests along with the existing build job. - -## Continuous Integration - -To ensure our changes don't break the build and all tests still pass, we use -Continuous Integration (CI) to run these checks automatically for every push. -Read through this article to understand [Continuous Integration, Continuous Delivery, and Continuous Deployment](https://about.gitlab.com/blog/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/), -and how these methods are leveraged by GitLab. -From the [last tutorial](https://ryanhallcs.wordpress.com/2017/03/15/devops-and-game-dev/) we already have a `.gitlab-ci.yml` file set up for building our app from -every push. We need to set up a new CI job for testing, which GitLab CI/CD will run after the build job using our generated artifacts from gulp. - -Please read through the [documentation on CI/CD configuration](../../../ci/yaml/README.md) file to explore its contents and adjust it to your needs. - -### Build your game with GitLab CI/CD - -We need to update our build job to ensure tests get run as well. Add `gulp build-test` -to the end of the `script` array for the existing `build` job. After these commands run, -we know we will need to access everything in the `built` folder, given by GitLab CI/CD's `artifacts`. -We'll also cache `node_modules` to avoid having to do a full re-pull of those dependencies: -just pack them up in the cache. Here is the full `build` job: - -```yaml -build: - stage: build - script: - - npm i gulp -g - - npm i - - gulp - - gulp build-test - cache: - policy: push - paths: - - node_modules - artifacts: - paths: - - built -``` - -### Test your game with GitLab CI/CD - -For testing locally, we simply run `gulp run-tests`, which requires gulp to be installed -globally like in the `build` job. We pull `node_modules` from the cache, so the `npm i` -command won't have to do much. In preparation for deployment, we know we will still need -the `built` folder in the artifacts, which will be brought over as default behavior from -the previous job. Lastly, by convention, we let GitLab CI/CD know this needs to be run after -the `build` job by giving it a `test` [stage](../../../ci/yaml/README.md#stages). -Following the YAML structure, the `test` job should look like this: - -```yaml -test: - stage: test - script: - - npm i gulp -g - - npm i - - gulp run-test - cache: - policy: push - paths: - - node_modules/ - artifacts: - paths: - - built/ -``` - -We have added unit tests for a `Weapon` class that shoots on a specified interval. -The `Player` class implements `Weapon` along with the ability to move around and shoot. Also, -we've added test artifacts and a test stage to our GitLab CI/CD pipeline using `.gitlab-ci.yml`, -allowing us to run our tests by every push. -Our entire `.gitlab-ci.yml` file should now look like this: - -```yaml -image: node:10 - -build: - stage: build - script: - - npm i gulp -g - - npm i - - gulp - - gulp build-test - cache: - policy: push - paths: - - node_modules/ - artifacts: - paths: - - built/ - -test: - stage: test - script: - - npm i gulp -g - - npm i - - gulp run-test - cache: - policy: pull - paths: - - node_modules/ - artifacts: - paths: - - built/ -``` - -### Run your CI/CD pipeline - -That's it! Add all your new files, commit, and push. For a reference of what our repository should -look like at this point, please refer to the [final commit related to this article on my sample repository](https://gitlab.com/blitzgren/gitlab-game-demo/commit/8b36ef0ecebcf569aeb251be4ee13743337fcfe2). -By applying both build and test stages, GitLab will run them sequentially at every push to -our repository. If all goes well you'll end up with a green check mark on each job for the pipeline: - -![Passing Pipeline](img/test_pipeline_pass.png) - -You can confirm that the tests passed by clicking on the `test` job to enter the full build logs. -Scroll to the bottom and observe, in all its passing glory: - -```shell -$ gulp run-test -[18:37:24] Using gulpfile /builds/blitzgren/gitlab-game-demo/gulpfile.js -[18:37:24] Starting 'run-test'... -[18:37:24] Finished 'run-test' after 21 ms - - - Weapon - ✓ should shoot if not in cooldown - ✓ should not shoot during cooldown - ✓ should shoot after cooldown ends - ✓ should not shoot if not triggered - - - 4 passing (18ms) - -Uploading artifacts... -built/: found 17 matching files -Uploading artifacts to coordinator... ok id=17095874 responseStatus=201 Created token=aaaaaaaa Job succeeded -``` - -## Continuous Deployment - -We have our codebase built and tested on every push. To complete the full pipeline with Continuous Deployment, -let's set up [free web hosting with AWS S3](https://aws.amazon.com/free/) and a job through which our build artifacts get -deployed. GitLab also has a free static site hosting service we can use, [GitLab Pages](https://about.gitlab.com/stages-devops-lifecycle/pages/), -however Dark Nova specifically uses other AWS tools that necessitates using `AWS S3`. -Read through this article that describes [deploying to both S3 and GitLab Pages](https://about.gitlab.com/blog/2016/08/26/ci-deployment-and-environments/) -and further delves into the principles of GitLab CI/CD than discussed in this article. - -### Set up S3 Bucket - -1. Log into your AWS account and go to [S3](https://console.aws.amazon.com/s3/home) -1. Click the **Create Bucket** link at the top -1. Enter a name of your choosing and click next -1. Keep the default **Properties** and click next -1. Click the **Manage group permissions** and allow **Read** for the **Everyone** group, click next -1. Create the bucket, and select it in your S3 bucket list -1. On the right side, click **Properties** and enable the **Static website hosting** category -1. Update the radio button to the **Use this bucket to host a website** selection. Fill in `index.html` and `error.html` respectively - -### Set up AWS Secrets - -We need to be able to deploy to AWS with our AWS account credentials, but we certainly -don't want to put secrets into source code. Luckily GitLab provides a solution for this -with [Variables](../../../ci/variables/README.md). This can get complicated -due to [IAM](https://aws.amazon.com/iam/) management. As a best practice, you shouldn't -use root security credentials. Proper IAM credential management is beyond the scope of this -article, but AWS will remind you that using root credentials is unadvised and against their -best practices, as they should. Feel free to follow best practices and use a custom IAM user's -credentials, which will be the same two credentials (Key ID and Secret). It's a good idea to -fully understand [IAM Best Practices in AWS](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html). We need to add these credentials to GitLab: - -1. Log into your AWS account and go to the [Security Credentials page](https://console.aws.amazon.com/iam/home#/security_credential) -1. Click the **Access Keys** section and **Create New Access Key**. Create the key and keep the ID and secret around, you'll need them later - - ![AWS Access Key Configuration](img/aws_config_window.png) - -1. Go to your GitLab project, click **Settings > CI/CD** on the left sidebar -1. Expand the **Variables** section - - ![GitLab Secret Configuration](img/gitlab_config.png) - -1. Add a key named `AWS_KEY_ID` and copy the key ID from Step 2 into the **Value** field -1. Add a key named `AWS_KEY_SECRET` and copy the key secret from Step 2 into the **Value** field - -### Deploy your game with GitLab CI/CD - -To deploy our build artifacts, we need to install the [AWS CLI](https://aws.amazon.com/cli/) on -the shared runner. The shared runner also needs to be able to authenticate with your AWS -account to deploy the artifacts. By convention, AWS CLI will look for `AWS_ACCESS_KEY_ID` -and `AWS_SECRET_ACCESS_KEY`. GitLab CI/CD gives us a way to pass the variables we -set up in the prior section using the `variables` portion of the `deploy` job. At the end, -we add directives to ensure deployment `only` happens on pushes to `master`. This way, every -single branch still runs through CI, and only merging (or committing directly) to master will -trigger the `deploy` job of our pipeline. Put these together to get the following: - -```yaml -deploy: - stage: deploy - variables: - AWS_ACCESS_KEY_ID: "$AWS_KEY_ID" - AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET" - script: - - apt-get update - - apt-get install -y python3-dev python3-pip - - easy_install3 -U pip - - pip3 install --upgrade awscli - - aws s3 sync ./built s3://gitlab-game-demo --region "us-east-1" --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --cache-control "no-cache, no-store, must-revalidate" --delete - only: - - master -``` - -Be sure to update the region and S3 URL in that last script command to fit your setup. -Our final configuration file `.gitlab-ci.yml` looks like: - -```yaml -image: node:10 - -build: - stage: build - script: - - npm i gulp -g - - npm i - - gulp - - gulp build-test - cache: - policy: push - paths: - - node_modules/ - artifacts: - paths: - - built/ - -test: - stage: test - script: - - npm i gulp -g - - gulp run-test - cache: - policy: pull - paths: - - node_modules/ - artifacts: - paths: - - built/ - -deploy: - stage: deploy - variables: - AWS_ACCESS_KEY_ID: "$AWS_KEY_ID" - AWS_SECRET_ACCESS_KEY: "$AWS_KEY_SECRET" - script: - - apt-get update - - apt-get install -y python3-dev python3-pip - - easy_install3 -U pip - - pip3 install --upgrade awscli - - aws s3 sync ./built s3://gitlab-game-demo --region "us-east-1" --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --cache-control "no-cache, no-store, must-revalidate" --delete - only: - - master -``` - -## Conclusion - -Within the [demo repository](https://gitlab.com/blitzgren/gitlab-game-demo) you can also find a handful of boilerplate code to get -[TypeScript](https://www.typescriptlang.org/), [Mocha](https://mochajs.org/), [Gulp](https://gulpjs.com/) and [Phaser](https://phaser.io) all playing -together nicely with GitLab CI/CD, which is the result of lessons learned while making [Dark Nova](https://www.darknova.io). -Using a combination of free and open source software, we have a full CI/CD pipeline, a game foundation, -and unit tests, all running and deployed at every push to master - with shockingly little code. -Errors can be easily debugged through GitLab build logs, and within minutes of a successful commit, -you can see the changes live on your game. - -Setting up Continuous Integration and Continuous Deployment from the start with Dark Nova enables -rapid but stable development. We can easily test changes in a separate [environment](../../environments/index.md), -or multiple environments if needed. Balancing and updating a multiplayer game can be ongoing -and tedious, but having faith in a stable deployment with GitLab CI/CD allows -a lot of breathing room in quickly getting changes to players. - -## Further settings - -Here are some ideas to further investigate that can speed up or improve your pipeline: - -- [Yarn](https://yarnpkg.com) instead of npm -- Set up a custom [Docker](../../../ci/docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) image that can pre-load dependencies and tools (like AWS CLI) -- Forward a [custom domain](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) to your game's S3 static website -- Combine jobs if you find it unnecessary for a small project -- Avoid the queues and set up your own [custom GitLab CI/CD runner](https://about.gitlab.com/blog/2016/03/01/gitlab-runner-with-docker/) +<!-- This redirect file can be deleted after 2021-04-19. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/examples/test-phoenix-application.md b/doc/ci/examples/test-phoenix-application.md index 52db5740c34..42b7681bd10 100644 --- a/doc/ci/examples/test-phoenix-application.md +++ b/doc/ci/examples/test-phoenix-application.md @@ -3,3 +3,6 @@ redirect_to: '../../ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md' --- The content of this page was incorporated in [this document](../../ci/examples/test_phoenix_app_with_gitlab_ci_cd/index.md). + +<!-- This redirect file can be deleted after February 1, 2021. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md index 5c499d6a855..a1a7de26cf2 100644 --- a/doc/ci/examples/test-scala-application.md +++ b/doc/ci/examples/test-scala-application.md @@ -1,81 +1,8 @@ --- -stage: Verify -group: Continuous Integration -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments -type: tutorial +redirect_to: '../README.md#contributed-examples' --- -# Test and deploy a Scala application to Heroku +This document was moved to [another location](../README.md#contributed-examples). -This example demonstrates the integration of GitLab CI/CD with Scala -applications using SBT. You can view or fork the [example project](https://gitlab.com/gitlab-examples/scala-sbt) -and view the logs of its past [CI jobs](https://gitlab.com/gitlab-examples/scala-sbt/-/jobs?scope=finished). - -## Add `.gitlab-ci.yml` file to project - -The following `.gitlab-ci.yml` should be added in the root of your -repository to trigger CI: - -``` yaml -image: openjdk:8 - -stages: - - test - - deploy - -before_script: - - apt-get update -y - - apt-get install apt-transport-https -y - ## Install SBT - - echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list - - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823 - - apt-get update -y - - apt-get install sbt -y - - sbt sbtVersion - -test: - stage: test - script: - - sbt clean coverage test coverageReport - -deploy: - stage: deploy - script: - - apt-get update -yq - - apt-get install rubygems ruby-dev -y - - gem install dpl - - dpl --provider=heroku --app=gitlab-play-sample-app --api-key=$HEROKU_API_KEY -``` - -In the above configuration: - -- The `before_script` installs [SBT](https://www.scala-sbt.org/) and - displays the version that is being used. -- The `test` stage executes SBT to compile and test the project. - - [sbt-scoverage](https://github.com/scoverage/sbt-scoverage) is used as an SBT - plugin to measure test coverage. -- The `deploy` stage automatically deploys the project to Heroku using dpl. - -You can use other versions of Scala and SBT by defining them in -`build.sbt`. - -## Display test coverage in job - -Add the `Coverage was \[\d+.\d+\%\]` regular expression in the -**Settings > Pipelines > Coverage report** project setting to -retrieve the [test coverage](../pipelines/settings.md#test-coverage-report-badge) -rate from the build trace and have it displayed with your jobs. - -**Pipelines** must be enabled for this option to appear. - -## Heroku application - -A Heroku application is required. You can create one through the -[Dashboard](https://dashboard.heroku.com/). Substitute `gitlab-play-sample-app` -in the `.gitlab-ci.yml` file with your application's name. - -## Heroku API key - -You can look up your Heroku API key in your -[account](https://dashboard.heroku.com/account). Add a [protected variable](../variables/README.md#protect-a-custom-variable) with -this value in **Project ➔ Variables** with key `HEROKU_API_KEY`. +<!-- This redirect file can be deleted after 2021-04-18. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page --> diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 96bdad0cf82..56594103362 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -4450,22 +4450,31 @@ You can use [YAML anchors](#anchors) with [script](#script), [`before_script`](# and [`after_script`](#after_script) to use predefined commands in multiple jobs: ```yaml -.some-script: &some-script - - echo "Execute this script in `before_script` sections" - .some-script-before: &some-script-before - - echo "Execute this script in `script` sections" + - echo "Execute this script first" + +.some-script: &some-script + - echo "Execute this script second" + - echo "Execute this script too" .some-script-after: &some-script-after - - echo "Execute this script in `after_script` sections" + - echo "Execute this script last" -job_name: +job1: before_script: - *some-script-before script: - *some-script + - echo "Execute something, for this job only" after_script: - *some-script-after + +job2: + script: + - *some-script-before + - *some-script + - echo "Execute something else, for this job only" + - *some-script-after ``` #### YAML anchors for variables diff --git a/doc/development/README.md b/doc/development/README.md index 0d3c1b3cbe9..087b2b2e7eb 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -151,6 +151,7 @@ In these cases, use the following workflow: ## Backend guides +- [Directory structure](directory_structure.md) - [GitLab utilities](utilities.md) - [Issuable-like Rails models](issuable-like-models.md) - [Logging](logging.md) diff --git a/doc/development/directory_structure.md b/doc/development/directory_structure.md new file mode 100644 index 00000000000..c2329feb941 --- /dev/null +++ b/doc/development/directory_structure.md @@ -0,0 +1,36 @@ +--- +stage: none +group: unassigned +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Backend directory structure + +## Use namespaces to define bounded contexts + +A healthy application is divided into macro and sub components that represent the contexts at play, +whether they are related to business domain or infrastructure code. + +As GitLab code has so many features and components it's hard to see what contexts are involved. +We should expect any class to be defined inside a module/namespace that represents the contexts where it operates. + +When we namespace classes inside their domain: + +- Similar terminology becomes unambiguous as the domain clarifies the meaning: + For example, `MergeRequests::Diff` and `Notes::Diff`. +- Top-level namespaces could be associated to one or more groups identified as domain experts. +- We can better identify the interactions and coupling between components. + For example, several classes inside `MergeRequests::` domain interact more with `Ci::` + domain and less with `ImportExport::`. + +```ruby +# bad +class MyClass +end + +# good +module MyDomain + class MyClass + end +end +``` diff --git a/doc/development/usage_ping.md b/doc/development/usage_ping.md index 10c3de2f0a1..f3203379994 100644 --- a/doc/development/usage_ping.md +++ b/doc/development/usage_ping.md @@ -122,6 +122,60 @@ sequenceDiagram the hostname is `version.gitlab.com`, the protocol is `TCP`, and the port number is `443`, the required URL is <https://version.gitlab.com/>. +## Usage Ping Metric Life cycle + +### 1. New metrics addition + +Please follow the [Implementing Usage Ping](#implementing-usage-ping) guide. + +### 2. Existing metric change + +Because we do not control when customers update their self-managed instances of GitLab, +we **STRONGLY DISCOURAGE** changes to the logic used to calculate any metric. +Any such changes lead to inconsistent reports from multiple GitLab instances. +If there is a problem with an existing metric, it's best to deprecate the existing metric, +and use it, side by side, with the desired new metric. + +Example: +Consider following change. Before GitLab 12.6, the `example_metric` was implemented as: + +```ruby +{ + ... + example_metric: distinct_count(Project, :creator_id) +} +``` + +For GitLab 12.6, the metric was changed to filter out archived projects: + +```ruby +{ + ... + example_metric: distinct_count(Project.non_archived, :creator_id) +} +``` + +In this scenario all instances running up to GitLab 12.5 continue to report `example_metric`, +including all archived projects, while all instances running GitLab 12.6 and higher filters +out such projects. As Usage Ping data is collected from all reporting instances, the +resulting dataset includes mixed data, which distorts any following business analysis. + +The correct approach is to add a new metric for GitLab 12.6 release with updated logic: + +```ruby +{ + ... + example_metric_without_archived: distinct_count(Project.non_archived, :creator_id) +} +``` + +and update existing business analysis artefacts to use `example_metric_without_archived` instead of `example_metric` + +### 3. Metrics deprecation and removal + +The process for deprecating and removing metrics is currently under development. For +more information, see the following [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/284637). + ## Implementing Usage Ping Usage Ping consists of two kinds of data, counters and observations. Counters track how often a certain event @@ -446,6 +500,8 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF Aggregation on a `daily` basis does not pull more fine grained data. - `feature_flag`: optional. For details, see our [GitLab internal Feature flags](feature_flags/) documentation. +Use one of the following methods to track events: + 1. Track event in controller using `RedisTracking` module with `track_redis_hll_event(*controller_actions, name:, feature:, feature_default_enabled: false)`. Arguments: @@ -562,21 +618,6 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF api.trackRedisHllUserEvent('my_already_defined_event_name'), ``` -1. Track event using base module `Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name, values:)`. - - Arguments: - - - `event_name`: event name. - - `values`: One value or array of values we count. For example: user_id, visitor_id, user_ids. - -1. Track event on context level using base module `Gitlab::UsageDataCounters::HLLRedisCounter.track_event_in_context(event_name, values:, context:)`. - - Arguments: - - - `event_name`: event name. - - `values`: values we count. For example: user_id, visitor_id. - - `context`: context value. Allowed values are `default`, `free`, `bronze`, `silver`, `gold`, `starter`, `premium`, `ultimate` - 1. Get event data using `Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names:, start_date:, end_date:, context: '')`. Arguments: diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md index e0f5a400977..58730cd6682 100644 --- a/doc/user/packages/maven_repository/index.md +++ b/doc/user/packages/maven_repository/index.md @@ -186,10 +186,11 @@ published to the GitLab Package Registry. ## Authenticate to the Package Registry with Maven -To authenticate to the Package Registry, you need either a personal access token or deploy token. +To authenticate to the Package Registry, you need one of the following: -- If you use a [personal access token](../../../user/profile/personal_access_tokens.md), set the scope to `api`. -- If you use a [deploy token](../../project/deploy_tokens/index.md), set the scope to `read_package_registry`, `write_package_registry`, or both. +- A [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `api`. +- A [deploy token](../../project/deploy_tokens/index.md) with the scope set to `read_package_registry`, `write_package_registry`, or both. +- A [CI_JOB_TOKEN](#authenticate-with-a-ci-job-token-in-maven). ### Authenticate with a personal access token in Maven @@ -354,12 +355,13 @@ repositories { To use the GitLab endpoint for Maven packages, choose an option: -- **Project-level**: Use when you have few Maven packages and they are not in - the same GitLab group. -- **Group-level**: Use when you have many Maven packages in the same GitLab - group. -- **Instance-level**: Use when you have many Maven packages in different - GitLab groups or in their own namespace. +- **Project-level**: To publish Maven packages to a project, use a project-level endpoint. + To install Maven packages, use a project-level endpoint when you have few Maven packages + and they are not in the same GitLab group. +- **Group-level**: Use a group-level endpoint when you want to install packages from + many different projects in the same GitLab group. +- **Instance-level**: Use an instance-level endpoint when you want to install many + packages from different GitLab groups or in their own namespace. The option you choose determines the settings you add to your `pom.xml` file. @@ -533,7 +535,7 @@ repositories { After you have set up the [remote and authentication](#authenticate-to-the-package-registry-with-maven) and [configured your project](#use-the-gitlab-endpoint-for-maven-packages), -publish a Maven artifact from your project. +publish a Maven package to your project. ### Publish by using Maven @@ -604,11 +606,20 @@ To publish a package by using Gradle: Now navigate to your project's **Packages & Registries** page and view the published artifacts. +### Publishing a package with the same name or version + +When you publish a package with the same name or version as an existing package, +the existing package is overwritten. + ## Install a package To install a package from the GitLab Package Registry, you must configure the [remote and authenticate](#authenticate-to-the-package-registry-with-maven). -When this is completed, there are two ways to install a package. +When this is completed, you can install a package from a project, +group, or namespace. + +If multiple packages have the same name and version, when you install +a package, the most recently-published package is retrieved. ### Use Maven with `mvn install` diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 74311eefd83..d07905c0ead 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -119,7 +119,7 @@ and modify them if you have the necessary [permissions](../../permissions.md). > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17589) in GitLab 13.3. Assignees in the sidebar are updated in real time. This feature is **disabled by default**. -To enable, you need to enable [ActionCable in-app mode](https://docs.gitlab.com/omnibus/settings/actioncable.html). +To enable it, you need to enable [ActionCable in-app mode](https://docs.gitlab.com/omnibus/settings/actioncable.html). ### Issues List @@ -137,7 +137,22 @@ view, you can also make certain changes [in bulk](../bulk_editing.md) to the dis For more information, see the [Issue Data and Actions](issue_data_and_actions.md) page for a rundown of all the fields and information in an issue. -You can sort a list of issues in several ways, for example by issue creation date, milestone due date. For more information, see the [Sorting and Ordering Issue Lists](sorting_issue_lists.md) page. +You can sort a list of issues in several ways, for example by issue creation date, milestone due date. +For more information, see the [Sorting and ordering issue lists](sorting_issue_lists.md) page. + +#### Cached issue count + +> - [Introduced]([link-to-issue](https://gitlab.com/gitlab-org/gitlab/-/issues/243753)) in GitLab 13.9. +> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default. +> - It's disabled on GitLab.com. +> - It's not recommended for production use. +> - To use this feature in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-cached-issue-count) **(CORE ONLY)** + +WARNING: +This feature might not be available to you. Check the **version history** note above for details. + +In a group, the sidebar displays the total count of open issues and this value is cached if higher +than 1000. The cached value is rounded to thousands (or millions) and updated every 24 hours. ### Issue boards @@ -226,3 +241,22 @@ You can then see issue statuses in the [issue list](#issues-list) and the - [Issues API](../../../api/issues.md) - Configure an [external issue tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine, Bugzilla, or EWM. + +## Enable or disable cached issue count **(CORE ONLY)** + +Cached issue count in the left sidebar is under development and not ready for production use. It is +deployed behind a feature flag that is **disabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) +can enable it. + +To enable it: + +```ruby +Feature.enable(:cached_sidebar_open_issues_count) +``` + +To disable it: + +```ruby +Feature.disable(:cached_sidebar_open_issues_count) +``` diff --git a/lib/gitlab/experimentation/controller_concern.rb b/lib/gitlab/experimentation/controller_concern.rb index 4b1d3663095..2b38b12c914 100644 --- a/lib/gitlab/experimentation/controller_concern.rb +++ b/lib/gitlab/experimentation/controller_concern.rb @@ -140,7 +140,7 @@ module Gitlab cookies[:force_experiment].to_s.split(',').any? { |experiment| experiment.strip == experiment_key.to_s } end - def tracking_label(subject) + def tracking_label(subject = nil) return experimentation_subject_id if subject.blank? if subject.respond_to?(:to_global_id) diff --git a/lib/tasks/benchmark.rake b/lib/tasks/benchmark.rake new file mode 100644 index 00000000000..6deafb2c351 --- /dev/null +++ b/lib/tasks/benchmark.rake @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +return if Rails.env.production? + +namespace :benchmark do + desc 'Benchmark | Banzai pipeline/filters' + RSpec::Core::RakeTask.new(:banzai) do |t| + t.pattern = 'spec/benchmarks/banzai_benchmark.rb' + ENV['BENCHMARK'] = '1' + end +end diff --git a/lib/tasks/gitlab/pages.rake b/lib/tasks/gitlab/pages.rake index e15cbb4e32e..107e0d08b70 100644 --- a/lib/tasks/gitlab/pages.rake +++ b/lib/tasks/gitlab/pages.rake @@ -6,7 +6,8 @@ namespace :gitlab do task migrate_legacy_storage: :gitlab_environment do logger = Logger.new(STDOUT) logger.info('Starting to migrate legacy pages storage to zip deployments') - processed_projects = 0 + projects_migrated = 0 + projects_errored = 0 ProjectPagesMetadatum.only_on_legacy_storage.each_batch(of: 10) do |batch| batch.preload(project: [:namespace, :route, pages_metadatum: :pages_deployment]).each do |metadatum| @@ -16,20 +17,26 @@ namespace :gitlab do time = Benchmark.realtime do result = ::Pages::MigrateLegacyStorageToDeploymentService.new(project).execute end - processed_projects += 1 if result[:status] == :success logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time} seconds") + projects_migrated += 1 else logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time} seconds: #{result[:message]}") + projects_errored += 1 end rescue => e + projects_errored += 1 logger.error("#{e.message} project_id: #{project&.id}") Gitlab::ErrorTracking.track_exception(e, project_id: project&.id) end - logger.info("#{processed_projects} pages projects are processed") + logger.info("#{projects_migrated} projects are migrated successfully, #{projects_errored} projects failed to be migrated") end + + logger.info("A total of #{projects_migrated + projects_errored} projects were processed.") + logger.info("- The #{projects_migrated} projects migrated successfully") + logger.info("- The #{projects_errored} projects failed to be migrated") end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 317678714e3..b3fecb8f883 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4436,6 +4436,9 @@ msgstr "" msgid "Below you will find all the groups that are public." msgstr "" +msgid "Beta" +msgstr "" + msgid "Bi-weekly code coverage" msgstr "" @@ -13376,9 +13379,6 @@ msgstr "" msgid "GitLabPages|It may take up to 30 minutes before the site is available after the first deployment." msgstr "" -msgid "GitLabPages|Learn how to upload your static site and have it served by GitLab by following the %{link_start}documentation on GitLab Pages%{link_end}." -msgstr "" - msgid "GitLabPages|Learn more." msgstr "" @@ -13406,6 +13406,9 @@ msgstr "" msgid "GitLabPages|Save" msgstr "" +msgid "GitLabPages|See the %{docs_link_start}GitLab Pages documentation%{link_end} to learn how to upload your static site and have GitLab serve it. You can also follow a %{samples_link_start}sample project%{link_end} or use a %{templates_link_start}GitLab CI template%{link_end}." +msgstr "" + msgid "GitLabPages|Something went wrong while obtaining the Let's Encrypt certificate for %{domain}. To retry visit your %{link_start}domain details%{link_end}." msgstr "" @@ -14255,6 +14258,9 @@ msgstr "" msgid "GroupsNew|No import options available" msgstr "" +msgid "GroupsNew|Not all related objects are migrated, as %{docs_link_start}described here%{docs_link_end}. Please %{feedback_link_start}leave feedback%{feedback_link_end} on this feature." +msgstr "" + msgid "GroupsNew|Personal access token" msgstr "" diff --git a/package.json b/package.json index a5ca4851efa..202c63a63ea 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/svgs": "1.178.0", "@gitlab/tributejs": "1.0.0", - "@gitlab/ui": "25.11.3", + "@gitlab/ui": "25.12.2", "@gitlab/visual-review-tools": "1.6.1", "@rails/actioncable": "^6.0.3-4", "@rails/ujs": "^6.0.3-4", diff --git a/qa/Dockerfile b/qa/Dockerfile index d040bddfc7f..76c81d03071 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -3,8 +3,8 @@ LABEL maintainer="GitLab Quality Department <quality@gitlab.com>" ENV DEBIAN_FRONTEND="noninteractive" ENV DOCKER_VERSION="17.09.0-ce" -ENV CHROME_VERSION="84.0.4147.89-1" -ENV CHROME_DRIVER_VERSION="84.0.4147.30" +ENV CHROME_VERSION="87.0.4280.141-1" +ENV CHROME_DRIVER_VERSION="87.0.4280.88" ENV CHROME_DEB="google-chrome-stable_${CHROME_VERSION}_amd64.deb" ENV CHROME_URL="https://s3.amazonaws.com/gitlab-google-chrome-stable/${CHROME_DEB}" diff --git a/spec/benchmarks/banzai_benchmark.rb b/spec/benchmarks/banzai_benchmark.rb new file mode 100644 index 00000000000..e489237a2f2 --- /dev/null +++ b/spec/benchmarks/banzai_benchmark.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +if ENV.key?('BENCHMARK') + require 'spec_helper' + require 'erb' + require 'benchmark/ips' + + # This benchmarks some of the Banzai pipelines and filters. + # They are not definitive, but can be used by a developer to + # get a rough idea how the changing or addition of a new filter + # will effect performance. + # + # Run by: + # BENCHMARK=1 rspec spec/benchmarks/banzai_benchmark.rb + # or + # rake benchmark:banzai + # + RSpec.describe 'GitLab Markdown Benchmark', :aggregate_failures do + include MarkupHelper + + let_it_be(:feature) { MarkdownFeature.new } + let_it_be(:project) { feature.project } + let_it_be(:group) { feature.group } + let_it_be(:wiki) { feature.wiki } + let_it_be(:wiki_page) { feature.wiki_page } + let_it_be(:markdown_text) { feature.raw_markdown } + + let!(:render_context) { Banzai::RenderContext.new(project, current_user) } + + before do + stub_application_setting(asset_proxy_enabled: true) + stub_application_setting(asset_proxy_secret_key: 'shared-secret') + stub_application_setting(asset_proxy_url: 'https://assets.example.com') + stub_application_setting(asset_proxy_whitelist: %w(gitlab.com *.mydomain.com)) + + Banzai::Filter::AssetProxyFilter.initialize_settings + end + + context 'pipelines' do + it 'benchmarks several pipelines' do + path = 'images/example.jpg' + gitaly_wiki_file = Gitlab::GitalyClient::WikiFile.new(path: path) + allow(wiki).to receive(:find_file).with(path).and_return(Gitlab::Git::WikiFile.new(gitaly_wiki_file)) + allow(wiki).to receive(:wiki_base_path) { '/namespace1/gitlabhq/wikis' } + + puts "\n--> Benchmarking Full, Wiki, and Plain pipelines\n" + + Benchmark.ips do |x| + x.config(time: 10, warmup: 2) + + x.report('Full pipeline') { markdown(markdown_text, { pipeline: :full }) } + x.report('Wiki pipeline') { markdown(markdown_text, { pipeline: :wiki, wiki: wiki, page_slug: wiki_page.slug }) } + x.report('Plain pipeline') { markdown(markdown_text, { pipeline: :plain_markdown }) } + + x.compare! + end + end + end + + context 'filters' do + let(:context) do + tmp = { project: project, current_user: current_user, render_context: render_context } + Banzai::Filter::AssetProxyFilter.transform_context(tmp) + end + + it 'benchmarks all filters in the FullPipeline' do + benchmark_pipeline_filters(:full) + end + + it 'benchmarks all filters in the PlainMarkdownPipeline' do + benchmark_pipeline_filters(:plain_markdown) + end + end + + # build up the source text for each filter + def build_filter_text(pipeline, initial_text) + filter_source = {} + input_text = initial_text + + pipeline.filters.each do |filter_klass| + filter_source[filter_klass] = input_text + + output = filter_klass.call(input_text, context) + input_text = output + end + + filter_source + end + + def benchmark_pipeline_filters(pipeline_type) + pipeline = Banzai::Pipeline[pipeline_type] + filter_source = build_filter_text(pipeline, markdown_text) + + puts "\n--> Benchmarking #{pipeline.name.demodulize} filters\n" + + Benchmark.ips do |x| + x.config(time: 10, warmup: 2) + + pipeline.filters.each do |filter_klass| + label = filter_klass.name.demodulize.delete_suffix('Filter').truncate(20) + + x.report(label) { filter_klass.call(filter_source[filter_klass], context) } + end + + x.compare! + end + end + + # Fake a `current_user` helper + def current_user + feature.user + end + end +end diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb index 2e1bf27ba8b..98212df0c01 100644 --- a/spec/features/groups/import_export/connect_instance_spec.rb +++ b/spec/features/groups/import_export/connect_instance_spec.rb @@ -37,6 +37,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do ) expect(page).to have_content 'Import groups from another instance of GitLab' + expect(page).to have_content 'Not all related objects are migrated' fill_in :bulk_import_gitlab_url, with: source_url fill_in :bulk_import_gitlab_access_token, with: pat diff --git a/spec/finders/terraform/states_finder_spec.rb b/spec/finders/terraform/states_finder_spec.rb new file mode 100644 index 00000000000..260e5f4818f --- /dev/null +++ b/spec/finders/terraform/states_finder_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Terraform::StatesFinder do + describe '#execute' do + let_it_be(:project) { create(:project) } + let_it_be(:state_1) { create(:terraform_state, project: project) } + let_it_be(:state_2) { create(:terraform_state, project: project) } + + let(:user) { project.creator } + + subject { described_class.new(project, user).execute } + + it { is_expected.to contain_exactly(state_1, state_2) } + + context 'user does not have permission' do + let(:user) { create(:user) } + + before do + project.add_guest(user) + end + + it { is_expected.to be_empty } + end + + context 'filtering by name' do + let(:params) { { name: name_param } } + + subject { described_class.new(project, user, params: params).execute } + + context 'name does not match' do + let(:name_param) { 'other-name' } + + it { is_expected.to be_empty } + end + + context 'name does match' do + let(:name_param) { state_1.name } + + it { is_expected.to contain_exactly(state_1) } + end + end + end +end diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js index 66fc74525ad..c4301f63470 100644 --- a/spec/frontend/notes/stores/mutation_spec.js +++ b/spec/frontend/notes/stores/mutation_spec.js @@ -400,6 +400,19 @@ describe('Notes Store mutations', () => { expect(state.discussions[0].notes[0].note).toEqual('Foo'); }); + it('does not update existing note if it matches', () => { + const state = { + discussions: [{ ...individualNote, individual_note: false }], + }; + jest.spyOn(state.discussions[0].notes, 'splice'); + + const updated = individualNote.notes[0]; + + mutations.UPDATE_NOTE(state, updated); + + expect(state.discussions[0].notes.splice).not.toHaveBeenCalled(); + }); + it('transforms an individual note to discussion', () => { const state = { discussions: [individualNote], diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js index ee509eaad8d..01d5a99c037 100644 --- a/spec/frontend/search/mock_data.js +++ b/spec/frontend/search/mock_data.js @@ -26,7 +26,7 @@ export const MOCK_GROUPS = [ export const MOCK_PROJECT = { name: 'test project', - namespace_id: MOCK_GROUP.id, + namespace: MOCK_GROUP, nameWithNamespace: 'test group test project', id: 'test_1', }; @@ -34,13 +34,13 @@ export const MOCK_PROJECT = { export const MOCK_PROJECTS = [ { name: 'test project', - namespace_id: MOCK_GROUP.id, + namespace: MOCK_GROUP, name_with_namespace: 'test group test project', id: 'test_1', }, { name: 'test project 2', - namespace_id: MOCK_GROUP.id, + namespace: MOCK_GROUP, name_with_namespace: 'test group test project 2', id: 'test_2', }, diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js index c1fc61d7e89..f2ac8f2689d 100644 --- a/spec/frontend/search/topbar/components/project_filter_spec.js +++ b/spec/frontend/search/topbar/components/project_filter_spec.js @@ -99,7 +99,7 @@ describe('ProjectFilter', () => { it('calls setUrlParams with project id, group id, then calls visitUrl', () => { expect(setUrlParams).toHaveBeenCalledWith({ - [GROUP_DATA.queryParam]: MOCK_PROJECT.namespace_id, + [GROUP_DATA.queryParam]: MOCK_PROJECT.namespace.id, [PROJECT_DATA.queryParam]: MOCK_PROJECT.id, }); expect(visitUrl).toHaveBeenCalled(); diff --git a/spec/graphql/mutations/security/ci_configuration/configure_sast_spec.rb b/spec/graphql/mutations/security/ci_configuration/configure_sast_spec.rb new file mode 100644 index 00000000000..ed03a1cb906 --- /dev/null +++ b/spec/graphql/mutations/security/ci_configuration/configure_sast_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Security::CiConfiguration::ConfigureSast do + subject(:mutation) { described_class.new(object: nil, context: context, field: nil) } + + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:user) { create(:user) } + + let_it_be(:service_result_json) do + { + status: "success", + success_path: "http://127.0.0.1:3000/root/demo-historic-secrets/-/merge_requests/new?", + errors: nil + } + end + + let_it_be(:service_error_result_json) do + { + status: "error", + success_path: nil, + errors: %w(error1 error2) + } + end + + let(:context) do + GraphQL::Query::Context.new( + query: OpenStruct.new(schema: nil), + values: { current_user: user }, + object: nil + ) + end + + specify { expect(described_class).to require_graphql_authorizations(:push_code) } + + describe '#resolve' do + subject { mutation.resolve(project_path: project.full_path, configuration: {}) } + + let(:result) { subject } + + it 'raises an error if the resource is not accessible to the user' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when user does not have enough permissions' do + before do + project.add_guest(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when user is a maintainer of a different project' do + before do + create(:project_empty_repo).add_maintainer(user) + end + + it 'raises an error' do + expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when the user does not have permission to create a new branch' do + before_all do + project.add_developer(user) + end + + let(:error_message) { 'You are not allowed to create protected branches on this project.' } + + it 'returns an array of errors' do + allow_next_instance_of(::Files::MultiService) do |multi_service| + allow(multi_service).to receive(:execute).and_raise(Gitlab::Git::PreReceiveError.new("GitLab: #{error_message}")) + end + + expect(result).to match( + status: :error, + success_path: nil, + errors: match_array([error_message]) + ) + end + end + + context 'when the user can create a merge request' do + before_all do + project.add_developer(user) + end + + context 'when service successfully generates a path to create a new merge request' do + it 'returns a success path' do + allow_next_instance_of(::Security::CiConfiguration::SastCreateService) do |service| + allow(service).to receive(:execute).and_return(service_result_json) + end + + expect(result).to match( + status: 'success', + success_path: service_result_json[:success_path], + errors: [] + ) + end + end + + context 'when service can not generate any path to create a new merge request' do + it 'returns an array of errors' do + allow_next_instance_of(::Security::CiConfiguration::SastCreateService) do |service| + allow(service).to receive(:execute).and_return(service_error_result_json) + end + + expect(result).to match( + status: 'error', + success_path: be_nil, + errors: match_array(service_error_result_json[:errors]) + ) + end + end + end + end +end diff --git a/spec/graphql/resolvers/terraform/states_resolver_spec.rb b/spec/graphql/resolvers/terraform/states_resolver_spec.rb index 64b515528cd..91d48cd782b 100644 --- a/spec/graphql/resolvers/terraform/states_resolver_spec.rb +++ b/spec/graphql/resolvers/terraform/states_resolver_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Resolvers::Terraform::StatesResolver do include GraphqlHelpers - it { expect(described_class.type).to eq(Types::Terraform::StateType) } + it { expect(described_class).to have_nullable_graphql_type(Types::Terraform::StateType.connection_type) } it { expect(described_class.null).to be_truthy } describe '#resolve' do @@ -31,3 +31,21 @@ RSpec.describe Resolvers::Terraform::StatesResolver do end end end + +RSpec.describe Resolvers::Terraform::StatesResolver.single do + it { expect(described_class).to be < Resolvers::Terraform::StatesResolver } + + describe 'arguments' do + subject { described_class.arguments[argument] } + + describe 'name' do + let(:argument) { 'name' } + + it do + expect(subject).to be_present + expect(subject.type.to_s).to eq('String!') + expect(subject.description).to be_present + end + end + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 8f6a120110f..95c835773e1 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -318,6 +318,13 @@ RSpec.describe GitlabSchema.types['Project'] do it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) } end + describe 'terraform state field' do + subject { described_class.fields['terraformState'] } + + it { is_expected.to have_graphql_type(Types::Terraform::StateType) } + it { is_expected.to have_graphql_resolver(Resolvers::Terraform::StatesResolver.single) } + end + describe 'terraform states field' do subject { described_class.fields['terraformStates'] } diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 8eb1b7b3b3d..61aaa618c45 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -444,4 +444,82 @@ RSpec.describe GroupsHelper do end end end + + describe '#group_open_issues_count' do + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :public) } + let_it_be(:count_service) { Groups::OpenIssuesCountService } + + before do + allow(helper).to receive(:current_user) { current_user } + end + + context 'when cached_sidebar_open_issues_count feature flag is enabled' do + before do + stub_feature_flags(cached_sidebar_open_issues_count: true) + end + + it 'returns count value from cache' do + allow_next_instance_of(count_service) do |service| + allow(service).to receive(:count).and_return(2500) + end + + expect(helper.group_open_issues_count(group)).to eq('2.5k') + end + end + + context 'when cached_sidebar_open_issues_count feature flag is disabled' do + before do + stub_feature_flags(cached_sidebar_open_issues_count: false) + end + + it 'returns not cached issues count' do + allow(helper).to receive(:group_issues_count).and_return(2500) + + expect(helper.group_open_issues_count(group)).to eq('2,500') + end + end + end + + describe '#cached_open_group_issues_count' do + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, name: 'group') } + let_it_be(:count_service) { Groups::OpenIssuesCountService } + + before do + allow(helper).to receive(:current_user) { current_user } + end + + it 'returns all digits for count value under 1000' do + allow_next_instance_of(count_service) do |service| + allow(service).to receive(:count).and_return(999) + end + + expect(helper.cached_open_group_issues_count(group)).to eq('999') + end + + it 'returns truncated digits for count value over 1000' do + allow_next_instance_of(count_service) do |service| + allow(service).to receive(:count).and_return(2300) + end + + expect(helper.cached_open_group_issues_count(group)).to eq('2.3k') + end + + it 'returns truncated digits for count value over 10000' do + allow_next_instance_of(count_service) do |service| + allow(service).to receive(:count).and_return(12560) + end + + expect(helper.cached_open_group_issues_count(group)).to eq('12.6k') + end + + it 'returns truncated digits for count value over 100000' do + allow_next_instance_of(count_service) do |service| + allow(service).to receive(:count).and_return(112560) + end + + expect(helper.cached_open_group_issues_count(group)).to eq('112.6k') + end + end end diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb index ee7a30cd4cb..576021b37b3 100644 --- a/spec/helpers/invite_members_helper_spec.rb +++ b/spec/helpers/invite_members_helper_spec.rb @@ -7,6 +7,10 @@ RSpec.describe InviteMembersHelper do let_it_be(:developer) { create(:user, developer_projects: [project]) } let(:owner) { project.owner } + before do + helper.extend(Gitlab::Experimentation::ControllerConcern) + end + context 'with project' do before do assign(:project, project) @@ -202,7 +206,6 @@ RSpec.describe InviteMembersHelper do before do allow(helper).to receive(:experiment_tracking_category_and_group) { '_track_property_' } - allow(helper).to receive(:tracking_label) allow(helper).to receive(:current_user) { owner } end diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb index ed311314086..4eb03554378 100644 --- a/spec/models/terraform/state_spec.rb +++ b/spec/models/terraform/state_spec.rb @@ -25,6 +25,15 @@ RSpec.describe Terraform::State do it { expect(subject.map(&:name)).to eq(names.sort) } end + + describe '.with_name' do + let_it_be(:matching_name) { create(:terraform_state, name: 'matching-name') } + let_it_be(:other_name) { create(:terraform_state, name: 'other-name') } + + subject { described_class.with_name(matching_name.name) } + + it { is_expected.to contain_exactly(matching_name) } + end end describe '#destroy' do diff --git a/spec/requests/api/graphql/project/terraform/state_spec.rb b/spec/requests/api/graphql/project/terraform/state_spec.rb new file mode 100644 index 00000000000..9f1d9ab204a --- /dev/null +++ b/spec/requests/api/graphql/project/terraform/state_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'query a single terraform state' do + include GraphqlHelpers + include ::API::Helpers::RelatedResourcesHelpers + + let_it_be(:terraform_state) { create(:terraform_state, :with_version, :locked) } + + let(:latest_version) { terraform_state.latest_version } + let(:project) { terraform_state.project } + let(:current_user) { project.creator } + let(:data) { graphql_data.dig('project', 'terraformState') } + + let(:query) do + graphql_query_for( + :project, + { fullPath: project.full_path }, + query_graphql_field( + :terraformState, + { name: terraform_state.name }, + %{ + id + name + lockedAt + createdAt + updatedAt + + latestVersion { + id + serial + createdAt + updatedAt + + createdByUser { + id + } + + job { + name + } + } + + lockedByUser { + id + } + } + ) + ) + end + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + it 'returns terraform state data' do + expect(data).to match(a_hash_including({ + 'id' => global_id_of(terraform_state), + 'name' => terraform_state.name, + 'lockedAt' => terraform_state.locked_at.iso8601, + 'createdAt' => terraform_state.created_at.iso8601, + 'updatedAt' => terraform_state.updated_at.iso8601, + 'lockedByUser' => { 'id' => global_id_of(terraform_state.locked_by_user) }, + 'latestVersion' => { + 'id' => eq(global_id_of(latest_version)), + 'serial' => eq(latest_version.version), + 'createdAt' => eq(latest_version.created_at.iso8601), + 'updatedAt' => eq(latest_version.updated_at.iso8601), + 'createdByUser' => { 'id' => eq(global_id_of(latest_version.created_by_user)) }, + 'job' => { 'name' => eq(latest_version.build.name) } + } + })) + end + + context 'unauthorized users' do + let(:current_user) { nil } + + it { expect(data).to be_nil } + end +end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index c7756a4fae5..1c359b6e50f 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -54,7 +54,7 @@ RSpec.describe API::Groups do it_behaves_like 'invalid file upload request' end - context 'when file format is not supported' do + context 'when file is too large' do let(:file_path) { 'spec/fixtures/big-image.png' } let(:message) { 'is too big' } @@ -661,6 +661,7 @@ RSpec.describe API::Groups do describe 'PUT /groups/:id' do let(:new_group_name) { 'New Group'} + let(:file_path) { 'spec/fixtures/dk.png' } it_behaves_like 'group avatar upload' do def make_upload_request @@ -678,7 +679,8 @@ RSpec.describe API::Groups do request_access_enabled: true, project_creation_level: "noone", subgroup_creation_level: "maintainer", - default_branch_protection: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS + default_branch_protection: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS, + avatar: fixture_file_upload(file_path) } expect(response).to have_gitlab_http_status(:ok) @@ -701,6 +703,7 @@ RSpec.describe API::Groups do expect(json_response['shared_projects']).to be_an Array expect(json_response['shared_projects'].length).to eq(0) expect(json_response['default_branch_protection']).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + expect(json_response['avatar_url']).to end_with('dk.png') end context 'updating the `default_branch_protection` attribute' do diff --git a/spec/services/groups/open_issues_count_service_spec.rb b/spec/services/groups/open_issues_count_service_spec.rb new file mode 100644 index 00000000000..8bbb1c90c6b --- /dev/null +++ b/spec/services/groups/open_issues_count_service_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Groups::OpenIssuesCountService, :use_clean_rails_memory_store_caching do + let_it_be(:group) { create(:group, :public)} + let_it_be(:project) { create(:project, :public, namespace: group) } + let_it_be(:user) { create(:user) } + let_it_be(:issue) { create(:issue, :opened, project: project) } + let_it_be(:confidential) { create(:issue, :opened, confidential: true, project: project) } + let_it_be(:closed) { create(:issue, :closed, project: project) } + + subject { described_class.new(group, user) } + + describe '#relation_for_count' do + before do + allow(IssuesFinder).to receive(:new).and_call_original + end + + it 'uses the IssuesFinder to scope issues' do + expect(IssuesFinder) + .to receive(:new) + .with(user, group_id: group.id, state: 'opened', non_archived: true, include_subgroups: true, public_only: true) + + subject.count + end + end + + describe '#count' do + context 'when user is nil' do + it 'does not include confidential issues in the issue count' do + expect(described_class.new(group).count).to eq(1) + end + end + + context 'when user is provided' do + context 'when user can read confidential issues' do + before do + group.add_reporter(user) + end + + it 'returns the right count with confidential issues' do + expect(subject.count).to eq(2) + end + end + + context 'when user cannot read confidential issues' do + before do + group.add_guest(user) + end + + it 'does not include confidential issues' do + expect(subject.count).to eq(1) + end + end + + context 'with different cache values' do + let(:public_count_key) { subject.cache_key(described_class::PUBLIC_COUNT_KEY) } + let(:under_threshold) { described_class::CACHED_COUNT_THRESHOLD - 1 } + let(:over_threshold) { described_class::CACHED_COUNT_THRESHOLD + 1 } + + context 'when cache is empty' do + before do + Rails.cache.delete(public_count_key) + end + + it 'refreshes cache if value over threshold' do + allow(subject).to receive(:uncached_count).and_return(over_threshold) + + expect(subject.count).to eq(over_threshold) + expect(Rails.cache.read(public_count_key)).to eq(over_threshold) + end + + it 'does not refresh cache if value under threshold' do + allow(subject).to receive(:uncached_count).and_return(under_threshold) + + expect(subject.count).to eq(under_threshold) + expect(Rails.cache.read(public_count_key)).to be_nil + end + end + + context 'when cached count is under the threshold value' do + before do + Rails.cache.write(public_count_key, under_threshold) + end + + it 'does not refresh cache' do + expect(Rails.cache).not_to receive(:write) + expect(subject.count).to eq(under_threshold) + end + end + + context 'when cached count is over the threshold value' do + before do + Rails.cache.write(public_count_key, over_threshold) + end + + it 'does not refresh cache' do + expect(Rails.cache).not_to receive(:write) + expect(subject.count).to eq(over_threshold) + end + end + end + end + end +end diff --git a/spec/services/security/ci_configuration/sast_create_service_spec.rb b/spec/services/security/ci_configuration/sast_create_service_spec.rb new file mode 100644 index 00000000000..ff7ab614e08 --- /dev/null +++ b/spec/services/security/ci_configuration/sast_create_service_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::CiConfiguration::SastCreateService, :snowplow do + describe '#execute' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let(:params) { {} } + + subject(:result) { described_class.new(project, user, params).execute } + + context 'user does not belong to project' do + it 'returns an error status' do + expect(result[:status]).to eq(:error) + expect(result[:success_path]).to be_nil + end + + it 'does not track a snowplow event' do + subject + + expect_no_snowplow_event + end + end + + context 'user belongs to project' do + before do + project.add_developer(user) + end + + it 'does track the snowplow event' do + subject + + expect_snowplow_event( + category: 'Security::CiConfiguration::SastCreateService', + action: 'create', + label: 'false' + ) + end + + it 'raises exception if the user does not have permission to create a new branch' do + allow(project).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, "You are not allowed to create protected branches on this project.") + + expect { subject }.to raise_error(Gitlab::Git::PreReceiveError) + end + + context 'with no parameters' do + it 'returns the path to create a new merge request' do + expect(result[:status]).to eq(:success) + expect(result[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/) + end + end + + context 'with parameters' do + let(:params) do + { 'stage' => 'security', + 'SEARCH_MAX_DEPTH' => 1, + 'SECURE_ANALYZERS_PREFIX' => 'new_registry', + 'SAST_EXCLUDED_PATHS' => 'spec,docs' } + end + + it 'returns the path to create a new merge request' do + expect(result[:status]).to eq(:success) + expect(result[:success_path]).to match(/#{Gitlab::Routing.url_helpers.project_new_merge_request_url(project, {})}(.*)description(.*)source_branch/) + end + end + end + end +end diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore index 259148fa18f..259148fa18f 100755..100644 --- a/vendor/gitignore/C++.gitignore +++ b/vendor/gitignore/C++.gitignore diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index a1c2a238a96..a1c2a238a96 100755..100644 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore diff --git a/yarn.lock b/yarn.lock index a075c24abb5..538c96146a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -876,10 +876,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8" integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw== -"@gitlab/ui@25.11.3": - version "25.11.3" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.11.3.tgz#54719d1276f417e66904f9f951671633f1647006" - integrity sha512-ur8UfgJ7giQZtp7pbVAwRYSWoxOzsFTpx/OpDge5EnmrH3S6YT0BOPxYs9T2HcMYN2Cejft1rhFJY+aPGxqxJA== +"@gitlab/ui@25.12.2": + version "25.12.2" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.12.2.tgz#ad47680da4b067140e8d48a04e807352660b9cca" + integrity sha512-y+uks00z+4kivTYu+l2mrjYT3nfnBS+xKWIUQ9xrkZVCC069V+DffPK+jVRzzhQ67hOMP5LVdaUEOcUplgFvGA== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.3.0" |