From 20d564f1064622ef0623434372ac3ceb03173331 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 5 Feb 2020 12:09:15 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .gitlab/ci/review.gitlab-ci.yml | 2 +- .../components/charts/stacked_column.vue | 103 +++ .../monitoring/components/panel_type.vue | 6 + app/assets/stylesheets/page_bundles/ide.scss | 10 + app/finders/concerns/time_frame_filter.rb | 14 + app/finders/milestones_finder.rb | 2 + .../concerns/time_frame_arguments.rb | 30 + app/graphql/resolvers/milestone_resolver.rb | 50 ++ app/graphql/types/group_type.rb | 4 + app/graphql/types/milestone_state_enum.rb | 8 + app/graphql/types/milestone_type.rb | 17 +- app/helpers/search_helper.rb | 13 - app/models/milestone.rb | 6 + app/presenters/milestone_presenter.rb | 15 + .../ci/create_job_artifacts_service.rb | 52 ++ app/views/search/_results.html.haml | 3 +- app/views/search/results/_blob.html.haml | 4 +- app/views/search/results/_wiki_blob.html.haml | 3 +- changelogs/unreleased/emails_disabled.yml | 5 + ...lly-handle-duplicate-artifacts-uploads.yml | 5 + changelogs/unreleased/issue_198425.yml | 5 + ...anvl-add-support-stacked-column-charts.yml | 5 + .../graphql/reference/gitlab_schema.graphql | 116 +++- doc/api/graphql/reference/gitlab_schema.json | 646 ++++++++++++------ doc/api/graphql/reference/index.md | 5 +- doc/api/projects.md | 3 + doc/ci/yaml/README.md | 16 + doc/development/architecture.md | 21 +- .../img/architecture_simplified.png | Bin 34330 -> 36325 bytes .../license_compliance/index.md | 76 ++- doc/user/packages/container_registry/index.md | 12 + lib/api/entities.rb | 1 + lib/api/helpers/projects_helpers.rb | 4 +- lib/api/runner.rb | 25 +- lib/api/search.rb | 4 - lib/gitlab/search/found_blob.rb | 1 + lib/gitlab/search/found_wiki_page.rb | 25 + package.json | 2 +- spec/finders/milestones_finder_spec.rb | 44 +- .../components/charts/stacked_column_spec.js | 45 ++ spec/frontend/monitoring/mock_data.js | 47 ++ .../resolvers/milestone_resolver_spec.rb | 93 +++ spec/lib/gitlab/search/found_blob_spec.rb | 10 + .../lib/gitlab/search/found_wiki_page_spec.rb | 18 + spec/models/abuse_report_spec.rb | 4 +- spec/models/award_emoji_spec.rb | 4 +- spec/models/blob_viewer/gitlab_ci_yml_spec.rb | 5 +- spec/models/ci/artifact_blob_spec.rb | 4 +- spec/models/ci/pipeline_spec.rb | 4 +- spec/models/clusters/cluster_spec.rb | 2 +- spec/models/commit_spec.rb | 2 +- spec/models/commit_status_spec.rb | 4 +- ...tch_destroy_dependent_associations_spec.rb | 8 +- spec/models/identity_spec.rb | 2 +- spec/models/label_note_spec.rb | 12 +- spec/models/lfs_file_lock_spec.rb | 2 +- spec/models/lfs_object_spec.rb | 10 +- spec/models/lfs_objects_project_spec.rb | 2 +- spec/models/merge_request_diff_spec.rb | 14 +- spec/models/merge_request_spec.rb | 4 +- spec/models/milestone_spec.rb | 17 +- spec/models/project_auto_devops_spec.rb | 2 +- spec/models/repository_spec.rb | 12 +- spec/models/sent_notification_spec.rb | 4 +- spec/models/user_spec.rb | 2 +- spec/presenters/milestone_presenter_spec.rb | 20 + .../api/graphql/group/milestones_spec.rb | 85 +++ spec/requests/api/projects_spec.rb | 17 +- spec/requests/api/runner_spec.rb | 42 +- .../ci/create_job_artifacts_service_spec.rb | 121 ++++ spec/support/helpers/graphql_helpers.rb | 3 +- .../within_timeframe_shared_examples.rb | 23 + yarn.lock | 11 +- 73 files changed, 1655 insertions(+), 363 deletions(-) create mode 100644 app/assets/javascripts/monitoring/components/charts/stacked_column.vue create mode 100644 app/finders/concerns/time_frame_filter.rb create mode 100644 app/graphql/resolvers/concerns/time_frame_arguments.rb create mode 100644 app/graphql/resolvers/milestone_resolver.rb create mode 100644 app/graphql/types/milestone_state_enum.rb create mode 100644 app/presenters/milestone_presenter.rb create mode 100644 app/services/ci/create_job_artifacts_service.rb create mode 100644 changelogs/unreleased/emails_disabled.yml create mode 100644 changelogs/unreleased/gracefully-handle-duplicate-artifacts-uploads.yml create mode 100644 changelogs/unreleased/issue_198425.yml create mode 100644 changelogs/unreleased/jivanvl-add-support-stacked-column-charts.yml create mode 100644 lib/gitlab/search/found_wiki_page.rb create mode 100644 spec/frontend/monitoring/components/charts/stacked_column_spec.js create mode 100644 spec/graphql/resolvers/milestone_resolver_spec.rb create mode 100644 spec/lib/gitlab/search/found_wiki_page_spec.rb create mode 100644 spec/presenters/milestone_presenter_spec.rb create mode 100644 spec/requests/api/graphql/group/milestones_spec.rb create mode 100644 spec/services/ci/create_job_artifacts_service_spec.rb create mode 100644 spec/support/shared_examples/policies/within_timeframe_shared_examples.rb diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index ad045d6c974..b970b590ae0 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -29,7 +29,7 @@ extends: - .default-tags - .default-retry - image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine + image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6 services: - docker:19.03.0-dind tags: diff --git a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue new file mode 100644 index 00000000000..55ae4a3bdb2 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue @@ -0,0 +1,103 @@ + + diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index 4d067365ed9..6751f3d31e8 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -15,6 +15,7 @@ import MonitorAnomalyChart from './charts/anomaly.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; import MonitorHeatmapChart from './charts/heatmap.vue'; import MonitorColumnChart from './charts/column.vue'; +import MonitorStackedColumnChart from './charts/stacked_column.vue'; import MonitorEmptyChart from './charts/empty_chart.vue'; import TrackEventDirective from '~/vue_shared/directives/track_event'; import { downloadCSVOptions, generateLinkToChartOptions } from '../utils'; @@ -24,6 +25,7 @@ export default { MonitorSingleStatChart, MonitorColumnChart, MonitorHeatmapChart, + MonitorStackedColumnChart, MonitorEmptyChart, Icon, GlDropdown, @@ -121,6 +123,10 @@ export default { v-else-if="isPanelType('column') && graphDataHasMetrics" :graph-data="graphData" /> + args[:end_date] + "startDate is after endDate" + end + + if error_message + raise Gitlab::Graphql::Errors::ArgumentError, error_message + end + end +end diff --git a/app/graphql/resolvers/milestone_resolver.rb b/app/graphql/resolvers/milestone_resolver.rb new file mode 100644 index 00000000000..2e7b6fdfd5f --- /dev/null +++ b/app/graphql/resolvers/milestone_resolver.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Resolvers + class MilestoneResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + include TimeFrameArguments + + argument :state, Types::MilestoneStateEnum, + required: false, + description: 'Filter milestones by state' + + type Types::MilestoneType, null: true + + def resolve(**args) + validate_timeframe_params!(args) + + authorize! + + MilestonesFinder.new(milestones_finder_params(args)).execute + end + + private + + def milestones_finder_params(args) + { + state: args[:state] || 'all', + start_date: args[:start_date], + end_date: args[:end_date] + }.merge(parent_id_parameter) + end + + def parent + @parent ||= object.respond_to?(:sync) ? object.sync : object + end + + def parent_id_parameter + if parent.is_a?(Group) + { group_ids: parent.id } + elsif parent.is_a?(Project) + { project_ids: parent.id } + end + end + + # MilestonesFinder does not check for current_user permissions, + # so for now we need to keep it here. + def authorize! + Ability.allowed?(context[:current_user], :read_milestone, parent) || raise_resource_not_available_error! + end + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index d22983f2164..718770ebfbc 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -42,6 +42,10 @@ module Types field :parent, GroupType, null: true, description: 'Parent group', resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.parent_id).find } + + field :milestones, Types::MilestoneType.connection_type, null: true, + description: 'Find milestones', + resolver: Resolvers::MilestoneResolver end end diff --git a/app/graphql/types/milestone_state_enum.rb b/app/graphql/types/milestone_state_enum.rb new file mode 100644 index 00000000000..032571ac88f --- /dev/null +++ b/app/graphql/types/milestone_state_enum.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Types + class MilestoneStateEnum < BaseEnum + value 'active' + value 'closed' + end +end diff --git a/app/graphql/types/milestone_type.rb b/app/graphql/types/milestone_type.rb index 9c3afb28674..900f8c6f01d 100644 --- a/app/graphql/types/milestone_type.rb +++ b/app/graphql/types/milestone_type.rb @@ -3,25 +3,36 @@ module Types class MilestoneType < BaseObject graphql_name 'Milestone' + description 'Represents a milestone.' + + present_using MilestonePresenter authorize :read_milestone field :id, GraphQL::ID_TYPE, null: false, description: 'ID of the milestone' - field :description, GraphQL::STRING_TYPE, null: true, - description: 'Description of the milestone' + field :title, GraphQL::STRING_TYPE, null: false, description: 'Title of the milestone' - field :state, GraphQL::STRING_TYPE, null: false, + + field :description, GraphQL::STRING_TYPE, null: true, + description: 'Description of the milestone' + + field :state, Types::MilestoneStateEnum, null: false, description: 'State of the milestone' + field :web_path, GraphQL::STRING_TYPE, null: false, method: :milestone_path, + description: 'Web path of the milestone' + field :due_date, Types::TimeType, null: true, description: 'Timestamp of the milestone due date' + field :start_date, Types::TimeType, null: true, description: 'Timestamp of the milestone start date' field :created_at, Types::TimeType, null: false, description: 'Timestamp of milestone creation' + field :updated_at, Types::TimeType, null: false, description: 'Timestamp of last milestone update' end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 9a5c5f274a0..e478f76818f 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -86,19 +86,6 @@ module SearchHelper }).html_safe end - def find_project_for_result_blob(projects, result) - @project - end - - # Used in EE - def blob_projects(results) - nil - end - - def parse_search_result(result) - result - end - # Overriden in EE def search_blob_title(project, path) path diff --git a/app/models/milestone.rb b/app/models/milestone.rb index f709e518047..b3278f48aa9 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -59,6 +59,12 @@ class Milestone < ApplicationRecord where(project_id: projects).or(where(group_id: groups)) end + scope :within_timeframe, -> (start_date, end_date) do + where('start_date is not NULL or due_date is not NULL') + .where('start_date is NULL or start_date <= ?', end_date) + .where('due_date is NULL or due_date >= ?', start_date) + end + scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) } diff --git a/app/presenters/milestone_presenter.rb b/app/presenters/milestone_presenter.rb new file mode 100644 index 00000000000..7d9045ddebe --- /dev/null +++ b/app/presenters/milestone_presenter.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class MilestonePresenter < Gitlab::View::Presenter::Delegated + presents :milestone + + def milestone_path + url_builder.milestone_path(milestone) + end + + private + + def url_builder + @url_builder ||= Gitlab::UrlBuilder.new(milestone) + end +end diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb new file mode 100644 index 00000000000..e633dc7f633 --- /dev/null +++ b/app/services/ci/create_job_artifacts_service.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Ci + class CreateJobArtifactsService + ArtifactsExistError = Class.new(StandardError) + + def execute(job, artifacts_file, params, metadata_file: nil) + expire_in = params['expire_in'] || + Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in + + job.job_artifacts.build( + project: job.project, + file: artifacts_file, + file_type: params['artifact_type'], + file_format: params['artifact_format'], + file_sha256: artifacts_file.sha256, + expire_in: expire_in) + + if metadata_file + job.job_artifacts.build( + project: job.project, + file: metadata_file, + file_type: :metadata, + file_format: :gzip, + file_sha256: metadata_file.sha256, + expire_in: expire_in) + end + + job.update(artifacts_expire_in: expire_in) + rescue ActiveRecord::RecordNotUnique => error + return true if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file) + + Gitlab::ErrorTracking.track_exception(error, + job_id: job.id, + project_id: job.project_id, + uploading_type: params['artifact_type'] + ) + + job.errors.add(:base, 'another artifact of the same type already exists') + false + end + + private + + def sha256_matches_existing_artifact?(job, artifact_type, artifacts_file) + existing_artifact = job.job_artifacts.find_by_file_type(artifact_type) + return false unless existing_artifact + + existing_artifact.file_sha256 == artifacts_file.sha256 + end + end +end diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 629a5a045b1..8ada8c875f7 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -32,8 +32,7 @@ .term = render 'shared/projects/list', projects: @search_objects, pipeline_status: false - else - - locals = { projects: blob_projects(@search_objects) } if %w[blobs wiki_blobs].include?(@scope) - = render partial: "search/results/#{@scope.singularize}", collection: @search_objects, locals: locals + = render partial: "search/results/#{@scope.singularize}", collection: @search_objects - if @scope != 'projects' = paginate_collection(@search_objects) diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index 4fb72b26955..6e17a25c713 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,7 +1,5 @@ -- project = find_project_for_result_blob(projects, blob) +- project = blob.project - return unless project - -- blob = parse_search_result(blob) - blob_link = project_blob_path(project, tree_join(blob.ref, blob.path)) = render partial: 'search/results/blob_data', locals: { blob: blob, project: project, path: blob.path, blob_link: blob_link } diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml index 9afed2bbecc..3040917dd6e 100644 --- a/app/views/search/results/_wiki_blob.html.haml +++ b/app/views/search/results/_wiki_blob.html.haml @@ -1,5 +1,4 @@ -- project = find_project_for_result_blob(projects, wiki_blob) -- wiki_blob = parse_search_result(wiki_blob) +- project = wiki_blob.project - wiki_blob_link = project_wiki_path(project, wiki_blob.basename) = render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, path: wiki_blob.path, blob_link: wiki_blob_link } diff --git a/changelogs/unreleased/emails_disabled.yml b/changelogs/unreleased/emails_disabled.yml new file mode 100644 index 00000000000..ac6b3dc3404 --- /dev/null +++ b/changelogs/unreleased/emails_disabled.yml @@ -0,0 +1,5 @@ +--- +title: Add emails_disabled to projects API +merge_request: 23616 +author: Mathieu Parent +type: added diff --git a/changelogs/unreleased/gracefully-handle-duplicate-artifacts-uploads.yml b/changelogs/unreleased/gracefully-handle-duplicate-artifacts-uploads.yml new file mode 100644 index 00000000000..884324e612c --- /dev/null +++ b/changelogs/unreleased/gracefully-handle-duplicate-artifacts-uploads.yml @@ -0,0 +1,5 @@ +--- +title: Replace artifacts via Runner API if already exist +merge_request: 24165 +author: +type: fixed diff --git a/changelogs/unreleased/issue_198425.yml b/changelogs/unreleased/issue_198425.yml new file mode 100644 index 00000000000..7eae394f1e1 --- /dev/null +++ b/changelogs/unreleased/issue_198425.yml @@ -0,0 +1,5 @@ +--- +title: Expose group milestones on GraphQL +merge_request: 23635 +author: +type: added diff --git a/changelogs/unreleased/jivanvl-add-support-stacked-column-charts.yml b/changelogs/unreleased/jivanvl-add-support-stacked-column-charts.yml new file mode 100644 index 00000000000..f9ebced32a5 --- /dev/null +++ b/changelogs/unreleased/jivanvl-add-support-stacked-column-charts.yml @@ -0,0 +1,5 @@ +--- +title: Add support for stacked column charts +merge_request: 23474 +author: +type: changed diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 2c63ecfe08e..e04bb0fc13e 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -1735,8 +1735,8 @@ type Epic implements Noteable { before: String """ - List epics within a time frame where epics.end_date is between start_date - and end_date parameters (start_date parameter must be present) + List items within a time frame where items.end_date is between startDate and + endDate parameters (startDate parameter must be present) """ endDate: Time @@ -1776,8 +1776,8 @@ type Epic implements Noteable { sort: EpicSort """ - List epics within a time frame where epics.start_date is between start_date - and end_date parameters (end_date parameter must be present) + List items within a time frame where items.start_date is between startDate + and endDate parameters (endDate parameter must be present) """ startDate: Time @@ -2704,8 +2704,8 @@ type Group { authorUsername: String """ - List epics within a time frame where epics.end_date is between start_date - and end_date parameters (start_date parameter must be present) + List items within a time frame where items.end_date is between startDate and + endDate parameters (startDate parameter must be present) """ endDate: Time @@ -2735,8 +2735,8 @@ type Group { sort: EpicSort """ - List epics within a time frame where epics.start_date is between start_date - and end_date parameters (end_date parameter must be present) + List items within a time frame where items.start_date is between startDate + and endDate parameters (endDate parameter must be present) """ startDate: Time @@ -2766,8 +2766,8 @@ type Group { before: String """ - List epics within a time frame where epics.end_date is between start_date - and end_date parameters (start_date parameter must be present) + List items within a time frame where items.end_date is between startDate and + endDate parameters (startDate parameter must be present) """ endDate: Time @@ -2807,8 +2807,8 @@ type Group { sort: EpicSort """ - List epics within a time frame where epics.start_date is between start_date - and end_date parameters (end_date parameter must be present) + List items within a time frame where items.start_date is between startDate + and endDate parameters (endDate parameter must be present) """ startDate: Time @@ -2853,6 +2853,48 @@ type Group { """ mentionsDisabled: Boolean + """ + Find milestones + """ + milestones( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + List items within a time frame where items.end_date is between startDate and + endDate parameters (startDate parameter must be present) + """ + endDate: Time + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + List items within a time frame where items.start_date is between startDate + and endDate parameters (endDate parameter must be present) + """ + startDate: Time + + """ + Filter milestones by state + """ + state: MilestoneStateEnum + ): MilestoneConnection + """ Name of the namespace """ @@ -4457,6 +4499,9 @@ type Metadata { version: String! } +""" +Represents a milestone. +""" type Milestone { """ Timestamp of milestone creation @@ -4486,7 +4531,7 @@ type Milestone { """ State of the milestone """ - state: String! + state: MilestoneStateEnum! """ Title of the milestone @@ -4497,6 +4542,51 @@ type Milestone { Timestamp of last milestone update """ updatedAt: Time! + + """ + Web path of the milestone + """ + webPath: String! +} + +""" +The connection type for Milestone. +""" +type MilestoneConnection { + """ + A list of edges. + """ + edges: [MilestoneEdge] + + """ + A list of nodes. + """ + nodes: [Milestone] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type MilestoneEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Milestone +} + +enum MilestoneStateEnum { + active + closed } """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index a35bf8caccf..92e421eb53c 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -3150,6 +3150,26 @@ "name": "epic", "description": "Find a single epic", "args": [ + { + "name": "startDate", + "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "endDate", + "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "defaultValue": null + }, { "name": "iid", "description": "IID of the epic, e.g., \"1\"", @@ -3235,26 +3255,6 @@ } }, "defaultValue": null - }, - { - "name": "startDate", - "description": "List epics within a time frame where epics.start_date is between start_date and end_date parameters (end_date parameter must be present)", - "type": { - "kind": "SCALAR", - "name": "Time", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "endDate", - "description": "List epics within a time frame where epics.end_date is between start_date and end_date parameters (start_date parameter must be present)", - "type": { - "kind": "SCALAR", - "name": "Time", - "ofType": null - }, - "defaultValue": null } ], "type": { @@ -3269,6 +3269,26 @@ "name": "epics", "description": "Find epics", "args": [ + { + "name": "startDate", + "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "endDate", + "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "defaultValue": null + }, { "name": "iid", "description": "IID of the epic, e.g., \"1\"", @@ -3355,26 +3375,6 @@ }, "defaultValue": null }, - { - "name": "startDate", - "description": "List epics within a time frame where epics.start_date is between start_date and end_date parameters (end_date parameter must be present)", - "type": { - "kind": "SCALAR", - "name": "Time", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "endDate", - "description": "List epics within a time frame where epics.end_date is between start_date and end_date parameters (start_date parameter must be present)", - "type": { - "kind": "SCALAR", - "name": "Time", - "ofType": null - }, - "defaultValue": null - }, { "name": "after", "description": "Returns the elements in the list that come after the specified cursor.", @@ -3534,6 +3534,89 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "milestones", + "description": "Find milestones", + "args": [ + { + "name": "startDate", + "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "endDate", + "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "Filter milestones by state", + "type": { + "kind": "ENUM", + "name": "MilestoneStateEnum", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "MilestoneConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "name", "description": "Name of the namespace", @@ -3923,6 +4006,304 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "MilestoneConnection", + "description": "The connection type for Milestone.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "MilestoneEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Milestone", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "MilestoneEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Milestone", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Milestone", + "description": "Represents a milestone.", + "fields": [ + { + "name": "createdAt", + "description": "Timestamp of milestone creation", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": "Description of the milestone", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "dueDate", + "description": "Timestamp of the milestone due date", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "ID of the milestone", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "startDate", + "description": "Timestamp of the milestone start date", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "state", + "description": "State of the milestone", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "MilestoneStateEnum", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": "Title of the milestone", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": "Timestamp of last milestone update", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "webPath", + "description": "Web path of the milestone", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "MilestoneStateEnum", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "active", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "closed", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Epic", @@ -3950,6 +4331,26 @@ "name": "children", "description": "Children (sub-epics) of the epic", "args": [ + { + "name": "startDate", + "description": "List items within a time frame where items.start_date is between startDate and endDate parameters (endDate parameter must be present)", + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "endDate", + "description": "List items within a time frame where items.end_date is between startDate and endDate parameters (startDate parameter must be present)", + "type": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + }, + "defaultValue": null + }, { "name": "iid", "description": "IID of the epic, e.g., \"1\"", @@ -4036,26 +4437,6 @@ }, "defaultValue": null }, - { - "name": "startDate", - "description": "List epics within a time frame where epics.start_date is between start_date and end_date parameters (end_date parameter must be present)", - "type": { - "kind": "SCALAR", - "name": "Time", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "endDate", - "description": "List epics within a time frame where epics.end_date is between start_date and end_date parameters (start_date parameter must be present)", - "type": { - "kind": "SCALAR", - "name": "Time", - "ofType": null - }, - "defaultValue": null - }, { "name": "after", "description": "Returns the elements in the list that come after the specified cursor.", @@ -9583,151 +9964,6 @@ ], "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "Milestone", - "description": null, - "fields": [ - { - "name": "createdAt", - "description": "Timestamp of milestone creation", - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Time", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "description", - "description": "Description of the milestone", - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "dueDate", - "description": "Timestamp of the milestone due date", - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "Time", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "id", - "description": "ID of the milestone", - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "startDate", - "description": "Timestamp of the milestone start date", - "args": [ - - ], - "type": { - "kind": "SCALAR", - "name": "Time", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "state", - "description": "State of the milestone", - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "title", - "description": "Title of the milestone", - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "updatedAt", - "description": "Timestamp of last milestone update", - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "Time", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - - ], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "TaskCompletionStatus", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index a0d1787e816..00c610b9eb7 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -683,6 +683,8 @@ Autogenerated return type of MergeRequestSetWip ## Milestone +Represents a milestone. + | Name | Type | Description | | --- | ---- | ---------- | | `createdAt` | Time! | Timestamp of milestone creation | @@ -690,9 +692,10 @@ Autogenerated return type of MergeRequestSetWip | `dueDate` | Time | Timestamp of the milestone due date | | `id` | ID! | ID of the milestone | | `startDate` | Time | Timestamp of the milestone start date | -| `state` | String! | State of the milestone | +| `state` | MilestoneStateEnum! | State of the milestone | | `title` | String! | Title of the milestone | | `updatedAt` | Time! | Timestamp of last milestone update | +| `webPath` | String! | Web path of the milestone | ## Namespace diff --git a/doc/api/projects.md b/doc/api/projects.md index 61f29b1cd60..dbd21ef8c59 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1016,6 +1016,7 @@ POST /projects | `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` | +| `emails_disabled` | boolean | no | Disable email notifications | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `container_expiration_policy_attributes` | hash | no | Update the container expiration policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `enabled` (boolean) | @@ -1083,6 +1084,7 @@ POST /projects/user/:user_id | `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` | +| `emails_disabled` | boolean | no | Disable email notifications | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | @@ -1149,6 +1151,7 @@ PUT /projects/:id | `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `pages_access_level` | string | no | One of `disabled`, `private`, `enabled` or `public` | +| `emails_disabled` | boolean | no | Disable email notifications | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `container_expiration_policy_attributes` | hash | no | Update the container expiration policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `enabled` (boolean) | diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7006a62ff3e..cd6ee05f873 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -2150,6 +2150,11 @@ dashboards. It is not available for download through the web interface. ##### `artifacts:reports:license_management` **(ULTIMATE)** +CAUTION: **Warning:** +This artifact is still valid but was **deprecated** in favor of the +[artifacts:reports:license_scanning](#artifactsreportslicense_scanning-ultimate) +introduced in GitLab 12.8. + > Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above. The `license_management` report collects [Licenses](../../user/application_security/license_compliance/index.md) @@ -2159,6 +2164,17 @@ The collected License Compliance report will be uploaded to GitLab as an artifac be summarized in the merge requests and pipeline view. It is also used to provide data for security dashboards. It is not available for download through the web interface. +##### `artifacts:reports:license_scanning` **(ULTIMATE)** + +> Introduced in GitLab 12.8. Requires GitLab Runner 11.5 and above. + +The `license_scanning` report collects [Licenses](../../user/application_security/license_compliance/index.md) +as artifacts. + +The License Compliance report will be uploaded to GitLab as an artifact and will +be automatically shown in merge requests, pipeline view and provide data for security +dashboards. + ##### `artifacts:reports:performance` **(PREMIUM)** > Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above. diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 0cf311a645e..94321c3f8f7 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -52,17 +52,18 @@ graph TB Geo[GitLab Geo Node] -- TCP 22, 80, 443 --> NGINX GitLabShell --TCP 8080 -->Unicorn["Unicorn (GitLab Rails)"] - GitLabShell --> Gitaly + GitLabShell --> Praefect GitLabShell --> Redis Unicorn --> PgBouncer[PgBouncer] Unicorn --> Redis - Unicorn --> Gitaly + Unicorn --> Praefect Sidekiq --> Redis Sidekiq --> PgBouncer - Sidekiq --> Gitaly + Sidekiq --> Praefect GitLabWorkhorse[GitLab Workhorse] --> Unicorn GitLabWorkhorse --> Redis - GitLabWorkhorse --> Gitaly + GitLabWorkhorse --> Praefect + Praefect --> Gitaly NGINX --> GitLabWorkhorse NGINX -- TCP 8090 --> GitLabPages[GitLab Pages] NGINX --> Grafana[Grafana] @@ -128,6 +129,7 @@ Component statuses are linked to configuration documentation for each component. | [Unicorn (GitLab Rails)](#unicorn) | Handles requests for the web interface and API | [✅][unicorn-omnibus] | [✅][unicorn-charts] | [✅][unicorn-charts] | [✅](../user/gitlab_com/index.md#unicorn) | [⚙][unicorn-source] | [✅][gitlab-yml] | CE & EE | | [Sidekiq](#sidekiq) | Background jobs processor | [✅][sidekiq-omnibus] | [✅][sidekiq-charts] | [✅](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html) | [✅](../user/gitlab_com/index.md#sidekiq) | [✅][gitlab-yml] | [✅][gitlab-yml] | CE & EE | | [Gitaly](#gitaly) | Git RPC service for handling all Git calls made by GitLab | [✅][gitaly-omnibus] | [✅][gitaly-charts] | [✅][gitaly-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | [⚙][gitaly-source] | ✅ | CE & EE | +| [Praefect](#praefect) | A transparant proxy between any Git client and Gitaly storage nodes. | [✅][gitaly-omnibus] | [❌][gitaly-charts] | [❌][gitaly-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | [⚙][praefect-source] | ✅ | CE & EE | | [GitLab Workhorse](#gitlab-workhorse) | Smart reverse proxy, handles large HTTP requests | [✅][workhorse-omnibus] | [✅][workhorse-charts] | [✅][workhorse-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | [⚙][workhorse-source] | ✅ | CE & EE | | [GitLab Shell](#gitlab-shell) | Handles `git` over SSH sessions | [✅][shell-omnibus] | [✅][shell-charts] | [✅][shell-charts] | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | [⚙][shell-source] | [✅][gitlab-yml] | CE & EE | | [GitLab Pages](#gitlab-pages) | Hosts static websites | [⚙][pages-omnibus] | [❌][pages-charts] | [❌][pages-charts] | [✅](../user/gitlab_com/index.md#gitlab-pages) | [⚙][pages-source] | [⚙][pages-gdk] | CE & EE | @@ -220,6 +222,16 @@ Elasticsearch is a distributed RESTful search engine built for the cloud. Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab (think GitLab.com or High Availability Deployments). As of 11.3.0, this service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly). +#### Praefect + +- [Project page](https://gitlab.com/gitlab-org/gitaly/blob/master/README.md) +- Configuration: [Omnibus][gitaly-omnibus], [Source][praefect-source] +- Layer: Core Service (Data) +- Process: `praefect` + +Praefect is a transparent proxy between each Git client and the Gitaly coordinating the replication of +repository updates to secondairy nodes. + #### GitLab Geo - Configuration: [Omnibus][geo-omnibus], [Charts][geo-charts], [GDK][geo-gdk] @@ -641,6 +653,7 @@ We've also detailed [our architecture of GitLab.com](https://about.gitlab.com/ha [gitaly-omnibus]: ../administration/gitaly/index.md [gitaly-charts]: https://docs.gitlab.com/charts/charts/gitlab/gitaly/ [gitaly-source]: ../install/installation.md#install-gitaly +[praefect-source]: ../install/installation.md#install-gitaly [workhorse-omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template [workhorse-charts]: https://docs.gitlab.com/charts/charts/gitlab/unicorn/ [workhorse-source]: ../install/installation.md#install-gitlab-workhorse diff --git a/doc/development/img/architecture_simplified.png b/doc/development/img/architecture_simplified.png index 1ad57b654687e5e65df08d77b34f9cabfa4ac299..4899993310f177838bef018a9b3a1beea23372cd 100644 GIT binary patch literal 36325 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 diff --git a/doc/user/application_security/license_compliance/index.md b/doc/user/application_security/license_compliance/index.md index ee8c4b8774c..95eec0db7fa 100644 --- a/doc/user/application_security/license_compliance/index.md +++ b/doc/user/application_security/license_compliance/index.md @@ -26,7 +26,7 @@ licenses in your project's settings. NOTE: **Note:** If the license compliance report doesn't have anything to compare to, no information will be displayed in the merge request area. That is the case when you add the -`license_management` job in your `.gitlab-ci.yml` for the first time. +`license_scanning` job in your `.gitlab-ci.yml` for the first time. Consecutive merge requests will have something to compare to and the license compliance report will be shown properly. @@ -70,25 +70,38 @@ To run a License Compliance scanning job, you need GitLab Runner with the ## Configuration -For GitLab 11.9 and later, to enable License Compliance, you must +For GitLab 12.8 and later, to enable License Compliance, you must [include](../../../ci/yaml/README.md#includetemplate) the -[`License-Management.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml) +[`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml) that's provided as a part of your GitLab installation. +For older versions of GitLab from 11.9 to 12.7, you must +[include](../../../ci/yaml/README.md#includetemplate) the +[`License-Management.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml). For GitLab versions earlier than 11.9, you can copy and use the job as defined that template. +NOTE: **Note:** +In GitLab 13.0, the `License-Management.gitlab-ci.yml` template is scheduled to be removed. +Use `License-Scanning.gitlab-ci.yml` instead. + Add the following to your `.gitlab-ci.yml` file: ```yaml include: - template: License-Management.gitlab-ci.yml + template: License-Scanning.gitlab-ci.yml ``` -The included template will create a `license_management` job in your CI/CD pipeline +The included template will create a `license_scanning` job in your CI/CD pipeline and scan your dependencies to find their licenses. +NOTE: **Note:** +Before GitLab 12.8, the `license_scanning` job was named `license_management`. +In GitLab 13.0, the `license_management` job is scheduled to be removed completely, +so you're advised to migrate to the `license_scanning` job and used the new +`License-Scanning.gitlab-ci.yml` template. + The results will be saved as a -[License Compliance report artifact](../../../ci/yaml/README.md#artifactsreportslicense_management-ultimate) +[License Compliance report artifact](../../../ci/yaml/README.md#artifactsreportslicense_scanning-ultimate) that you can later download and analyze. Due to implementation limitations, we always take the latest License Compliance artifact available. Behind the scenes, the [GitLab License Compliance Docker image](https://gitlab.com/gitlab-org/security-products/license-management) @@ -128,7 +141,7 @@ For example: ```yaml include: - template: License-Management.gitlab-ci.yml + template: License-Scanning.gitlab-ci.yml variables: LICENSE_MANAGEMENT_SETUP_CMD: sh my-custom-install-script.sh @@ -140,14 +153,14 @@ directory of your project. ### Overriding the template If you want to override the job definition (for example, change properties like -`variables` or `dependencies`), you need to declare a `license_management` job +`variables` or `dependencies`), you need to declare a `license_scanning` job after the template inclusion and specify any additional keys under it. For example: ```yaml include: - template: License-Management.gitlab-ci.yml + template: License-Scanning.gitlab-ci.yml -license_management: +license_scanning: variables: CI_DEBUG_TRACE: "true" ``` @@ -160,9 +173,9 @@ Feel free to use it for the customization of Maven execution. For example: ```yaml include: - template: License-Management.gitlab-ci.yml + template: License-Scanning.gitlab-ci.yml -license_management: +license_scanning: variables: MAVEN_CLI_OPTS: --debug ``` @@ -186,13 +199,48 @@ License Compliance uses Python 3.8 and pip 19.1 by default. If your project requires Python 2, you can switch to Python 2.7 and pip 10.0 by setting the `LM_PYTHON_VERSION` environment variable to `2`. +```yaml +include: + template: License-Scanning.gitlab-ci.yml + +license_scanning: + variables: + LM_PYTHON_VERSION: 2 +``` + +### Migration from `license_management` to `license_scanning` + +In GitLab 12.8 a new name for `license_management` job was introduced. This change was made to improve clarity around the purpose of the scan, which is to scan and collect the types of licenses present in a projects dependencies. +The support of `license_management` is scheduled to be dropped in GitLab 13.0. +If you're using a custom setup for License Compliance, you're required +to update your CI config accordingly: + +1. Change the CI template to `License-Scanning.gitlab-ci.yml`. +1. Change the job name to `license_management` (if you mention it in `.gitlab-ci.yml`). +1. Change the artifact name to `gl-license-scanning-report.json` (if you mention it in `.gitlab-ci.yml`). + +For example, the following `.gitlab-ci.yml`: + ```yaml include: template: License-Management.gitlab-ci.yml license_management: - variables: - LM_PYTHON_VERSION: 2 + artifacts: + reports: + license_management: gl-license-management-report.json +``` + +Should be changed to: + +```yaml +include: + template: License-Scanning.gitlab-ci.yml + +license_scanning: + artifacts: + reports: + license_scanning: gl-license-scanning-report.json ``` ## Project policies for License Compliance diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index 0238121f977..d072cb982c6 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -336,6 +336,18 @@ error during connect: Get http://docker:2376/v1.39/info: dial tcp: lookup docker It is possible to create a per-project expiration policy, so that you can make sure that older tags and images are regularly removed from the Container Registry. +The expiration policy algorithm starts by collecting all the tags for a given repository in a list, +then goes through a process of excluding tags from it until only the ones to be deleted remain: + +1. Collect all the tags for a given repository in a list. +1. Excludes the tag named `latest` from the list. +1. Evaluates the `name_regex`, excluding non-matching names from the list. +1. Excludes any tags that do not have a manifest (not part of the options). +1. Orders the remaining tags by `created_date`. +1. Excludes from the list the N tags based on the `keep_n` value (Expiration latest). +1. Excludes from the list the tags older than the `older_than` value (Expiration interval). +1. Finally, the remaining tags in the list are deleted from the Container Registry. + ### Managing project expiration policy through the API You can set, update, and disable the expiration policies using the GitLab API. diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 2da9a042978..2768dc103c4 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -64,6 +64,7 @@ module API expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) } expose(:pages_access_level) { |project, options| project.project_feature.string_access_level(:pages) } + expose :emails_disabled expose :shared_runners_enabled expose :lfs_enabled?, as: :lfs_enabled expose :creator_id diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 6717f33f3e1..c7c9f3ba077 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -29,6 +29,7 @@ module API optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`' optional :pages_access_level, type: String, values: %w(disabled private enabled public), desc: 'Pages access level. One of `disabled`, `private`, `enabled` or `public`' + optional :emails_disabled, type: Boolean, desc: 'Disable email notifications' optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push' optional :remove_source_branch_after_merge, type: Boolean, desc: 'Remove the source branch by default after merge' @@ -87,6 +88,7 @@ module API def self.update_params_at_least_one_of [ + :autoclose_referenced_issues, :auto_devops_enabled, :auto_devops_deploy_strategy, :auto_cancel_pending_pipelines, @@ -100,7 +102,7 @@ module API :container_expiration_policy_attributes, :default_branch, :description, - :autoclose_referenced_issues, + :emails_disabled, :issues_access_level, :lfs_enabled, :merge_requests_access_level, diff --git a/lib/api/runner.rb b/lib/api/runner.rb index 60cf9bf2c9c..42a8d891dce 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -276,29 +276,8 @@ module API bad_request!('Missing artifacts file!') unless artifacts file_too_large! unless artifacts.size < max_artifacts_size(job) - expire_in = params['expire_in'] || - Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in - - job.job_artifacts.build( - project: job.project, - file: artifacts, - file_type: params['artifact_type'], - file_format: params['artifact_format'], - file_sha256: artifacts.sha256, - expire_in: expire_in) - - if metadata - job.job_artifacts.build( - project: job.project, - file: metadata, - file_type: :metadata, - file_format: :gzip, - file_sha256: metadata.sha256, - expire_in: expire_in) - end - - if job.update(artifacts_expire_in: expire_in) - present Ci::BuildRunnerPresenter.new(job), with: Entities::JobRequest::Response + if Ci::CreateJobArtifactsService.new.execute(job, artifacts, params, metadata_file: metadata) + status :created else render_validation_error!(job) end diff --git a/lib/api/search.rb b/lib/api/search.rb index 6b74158930a..ed52a4fc8f2 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -32,10 +32,6 @@ module API results = SearchService.new(current_user, search_params).search_objects - process_results(results) - end - - def process_results(results) paginate(results) end diff --git a/lib/gitlab/search/found_blob.rb b/lib/gitlab/search/found_blob.rb index 360239a84e4..f472c70446c 100644 --- a/lib/gitlab/search/found_blob.rb +++ b/lib/gitlab/search/found_blob.rb @@ -7,6 +7,7 @@ module Gitlab include Presentable include BlobLanguageFromGitAttributes include Gitlab::Utils::StrongMemoize + include BlobActiveModel attr_reader :project, :content_match, :blob_path diff --git a/lib/gitlab/search/found_wiki_page.rb b/lib/gitlab/search/found_wiki_page.rb new file mode 100644 index 00000000000..99ca6a79fe2 --- /dev/null +++ b/lib/gitlab/search/found_wiki_page.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# - rendering by using data purely from Elasticsearch and does not trigger Gitaly calls. +# - allows policy check +module Gitlab + module Search + class FoundWikiPage < SimpleDelegator + attr_reader :wiki + + def self.declarative_policy_class + 'WikiPagePolicy' + end + + # @param found_blob [Gitlab::Search::FoundBlob] + def initialize(found_blob) + super + @wiki = found_blob.project.wiki + end + + def to_ability_name + 'wiki_page' + end + end + end +end diff --git a/package.json b/package.json index e8f4b24d518..7521958bc74 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.5.0", - "@gitlab/eslint-config": "^2.1.1", + "@gitlab/eslint-config": "^2.1.2", "@gitlab/eslint-plugin-i18n": "^1.1.0", "@gitlab/eslint-plugin-vue-i18n": "^1.2.0", "@vue/test-utils": "^1.0.0-beta.30", diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb index 3545ff35ed8..3402eb39b3b 100644 --- a/spec/finders/milestones_finder_spec.rb +++ b/spec/finders/milestones_finder_spec.rb @@ -3,13 +3,14 @@ require 'spec_helper' describe MilestonesFinder do + let(:now) { Time.now } let(:group) { create(:group) } let(:project_1) { create(:project, namespace: group) } let(:project_2) { create(:project, namespace: group) } - let!(:milestone_1) { create(:milestone, group: group, title: 'one test', due_date: Date.today) } - let!(:milestone_2) { create(:milestone, group: group) } - let!(:milestone_3) { create(:milestone, project: project_1, state: 'active', due_date: Date.tomorrow) } - let!(:milestone_4) { create(:milestone, project: project_2, state: 'active') } + let!(:milestone_1) { create(:milestone, group: group, title: 'one test', start_date: now - 1.day, due_date: now) } + let!(:milestone_2) { create(:milestone, group: group, start_date: now + 1.day, due_date: now + 2.days) } + let!(:milestone_3) { create(:milestone, project: project_1, state: 'active', start_date: now + 2.days, due_date: now + 3.days) } + let!(:milestone_4) { create(:milestone, project: project_2, state: 'active', start_date: now + 4.days, due_date: now + 5.days) } it 'returns milestones for projects' do result = described_class.new(project_ids: [project_1.id, project_2.id], state: 'all').execute @@ -33,8 +34,11 @@ describe MilestonesFinder do end it 'orders milestones by due date' do - expect(result.first).to eq(milestone_1) - expect(result.second).to eq(milestone_3) + milestone = create(:milestone, group: group, due_date: now - 2.days) + + expect(result.first).to eq(milestone) + expect(result.second).to eq(milestone_1) + expect(result.third).to eq(milestone_2) end end @@ -77,6 +81,34 @@ describe MilestonesFinder do expect(result.to_a).to contain_exactly(milestone_1) end + + context 'by timeframe' do + it 'returns milestones with start_date and due_date between timeframe' do + params.merge!(start_date: now - 1.day, end_date: now + 3.days) + + milestones = described_class.new(params).execute + + expect(milestones).to match_array([milestone_1, milestone_2, milestone_3]) + end + + it 'returns milestones which starts before the timeframe' do + milestone = create(:milestone, project: project_2, start_date: now - 5.days) + params.merge!(start_date: now - 3.days, end_date: now - 2.days) + + milestones = described_class.new(params).execute + + expect(milestones).to match_array([milestone]) + end + + it 'returns milestones which ends after the timeframe' do + milestone = create(:milestone, project: project_2, due_date: now + 6.days) + params.merge!(start_date: now + 6.days, end_date: now + 7.days) + + milestones = described_class.new(params).execute + + expect(milestones).to match_array([milestone]) + end + end end describe '#find_by' do diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js new file mode 100644 index 00000000000..abb89ac15ef --- /dev/null +++ b/spec/frontend/monitoring/components/charts/stacked_column_spec.js @@ -0,0 +1,45 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlStackedColumnChart } from '@gitlab/ui/dist/charts'; +import StackedColumnChart from '~/monitoring/components/charts/stacked_column.vue'; +import { stackedColumnMockedData } from '../../mock_data'; + +jest.mock('~/lib/utils/icon_utils', () => ({ + getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'), +})); + +describe('Stacked column chart component', () => { + let wrapper; + const glStackedColumnChart = () => wrapper.find(GlStackedColumnChart); + + beforeEach(() => { + wrapper = shallowMount(StackedColumnChart, { + propsData: { + graphData: stackedColumnMockedData, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('with graphData present', () => { + it('is a Vue instance', () => { + expect(glStackedColumnChart().exists()).toBe(true); + }); + + it('should contain the same number of elements in the seriesNames computed prop as the graphData metrics prop', () => + wrapper.vm + .$nextTick() + .then(expect(wrapper.vm.seriesNames).toHaveLength(stackedColumnMockedData.metrics.length))); + + it('should contain the same number of elements in the groupBy computed prop as the graphData result prop', () => + wrapper.vm + .$nextTick() + .then( + expect(wrapper.vm.groupBy).toHaveLength( + stackedColumnMockedData.metrics[0].result[0].values.length, + ), + )); + }); +}); diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 5fd73b73e0d..0c985ba4fca 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -665,3 +665,50 @@ export const graphDataPrometheusQueryRangeMultiTrack = { }, ], }; + +export const stackedColumnMockedData = { + title: 'memories', + type: 'stacked-column', + x_label: 'x label', + y_label: 'y label', + metrics: [ + { + label: 'memory_1024', + unit: 'count', + series_name: 'group 1', + prometheus_endpoint_path: + '/root/autodevops-deploy-6/-/environments/24/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024', + metric_id: 'undefined_metric_of_ages_1024', + metricId: 'undefined_metric_of_ages_1024', + result: [ + { + metric: {}, + values: [ + ['2020-01-30 12:00:00', '5'], + ['2020-01-30 12:01:00', '10'], + ['2020-01-30 12:02:00', '15'], + ], + }, + ], + }, + { + label: 'memory_1000', + unit: 'count', + series_name: 'group 2', + prometheus_endpoint_path: + '/root/autodevops-deploy-6/-/environments/24/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024', + metric_id: 'undefined_metric_of_ages_1000', + metricId: 'undefined_metric_of_ages_1000', + result: [ + { + metric: {}, + values: [ + ['2020-01-30 12:00:00', '20'], + ['2020-01-30 12:01:00', '25'], + ['2020-01-30 12:02:00', '30'], + ], + }, + ], + }, + ], +}; diff --git a/spec/graphql/resolvers/milestone_resolver_spec.rb b/spec/graphql/resolvers/milestone_resolver_spec.rb new file mode 100644 index 00000000000..297130c2027 --- /dev/null +++ b/spec/graphql/resolvers/milestone_resolver_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::MilestoneResolver do + include GraphqlHelpers + + describe '#resolve' do + let_it_be(:current_user) { create(:user) } + + context 'for group milestones' do + let_it_be(:now) { Time.now } + let_it_be(:group) { create(:group, :private) } + + def resolve_group_milestones(args = {}, context = { current_user: current_user }) + resolve(described_class, obj: group, args: args, ctx: context) + end + + before do + group.add_developer(current_user) + end + + it 'calls MilestonesFinder#execute' do + expect_next_instance_of(MilestonesFinder) do |finder| + expect(finder).to receive(:execute) + end + + resolve_group_milestones + end + + context 'without parameters' do + it 'calls MilestonesFinder to retrieve all milestones' do + expect(MilestonesFinder).to receive(:new) + .with(group_ids: group.id, state: 'all', start_date: nil, end_date: nil) + .and_call_original + + resolve_group_milestones + end + end + + context 'with parameters' do + it 'calls MilestonesFinder with correct parameters' do + start_date = now + end_date = start_date + 1.hour + + expect(MilestonesFinder).to receive(:new) + .with(group_ids: group.id, state: 'closed', start_date: start_date, end_date: end_date) + .and_call_original + + resolve_group_milestones(start_date: start_date, end_date: end_date, state: 'closed') + end + end + + context 'by timeframe' do + context 'when start_date and end_date are present' do + context 'when start date is after end_date' do + it 'raises error' do + expect do + resolve_group_milestones(start_date: now, end_date: now - 2.days) + end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "startDate is after endDate") + end + end + end + + context 'when only start_date is present' do + it 'raises error' do + expect do + resolve_group_milestones(start_date: now) + end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/) + end + end + + context 'when only end_date is present' do + it 'raises error' do + expect do + resolve_group_milestones(end_date: now) + end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/) + end + end + end + + context 'when user cannot read milestones' do + it 'raises error' do + unauthorized_user = create(:user) + + expect do + resolve_group_milestones({}, { current_user: unauthorized_user }) + end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + end +end diff --git a/spec/lib/gitlab/search/found_blob_spec.rb b/spec/lib/gitlab/search/found_blob_spec.rb index 07842faa638..ce6a54100a5 100644 --- a/spec/lib/gitlab/search/found_blob_spec.rb +++ b/spec/lib/gitlab/search/found_blob_spec.rb @@ -156,4 +156,14 @@ describe Gitlab::Search::FoundBlob do end end end + + describe 'policy' do + let(:project) { build(:project, :repository) } + + subject { described_class.new(project: project) } + + it 'works with policy' do + expect(Ability.allowed?(project.creator, :read_blob, subject)).to be_truthy + end + end end diff --git a/spec/lib/gitlab/search/found_wiki_page_spec.rb b/spec/lib/gitlab/search/found_wiki_page_spec.rb new file mode 100644 index 00000000000..e8b6728aba5 --- /dev/null +++ b/spec/lib/gitlab/search/found_wiki_page_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Search::FoundWikiPage do + let(:project) { create(:project, :public, :repository) } + + describe 'policy' do + let(:project) { build(:project, :repository) } + let(:found_blob) { Gitlab::Search::FoundBlob.new(project: project) } + + subject { described_class.new(found_blob) } + + it 'works with policy' do + expect(Ability.allowed?(project.creator, :read_wiki_page, subject)).to be_truthy + end + end +end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index 814df472389..2c4fa398636 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe AbuseReport do - set(:report) { create(:abuse_report) } - set(:user) { create(:admin) } + let_it_be(:report, reload: true) { create(:abuse_report) } + let_it_be(:user, reload: true) { create(:admin) } subject { report } it { expect(subject).to be_valid } diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb index b15b26b1630..b2d58dd95ad 100644 --- a/spec/models/award_emoji_spec.rb +++ b/spec/models/award_emoji_spec.rb @@ -45,8 +45,8 @@ describe AwardEmoji do end describe 'scopes' do - set(:thumbsup) { create(:award_emoji, name: 'thumbsup') } - set(:thumbsdown) { create(:award_emoji, name: 'thumbsdown') } + let_it_be(:thumbsup) { create(:award_emoji, name: 'thumbsup') } + let_it_be(:thumbsdown) { create(:award_emoji, name: 'thumbsdown') } describe '.upvotes' do it { expect(described_class.upvotes).to contain_exactly(thumbsup) } diff --git a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb index 02993052124..e645733e02d 100644 --- a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb +++ b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb @@ -6,9 +6,8 @@ describe BlobViewer::GitlabCiYml do include FakeBlobHelpers include RepoHelpers - set(:project) { create(:project, :repository) } - set(:user) { create(:user) } - + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } let(:blob) { fake_blob(path: '.gitlab-ci.yml', data: data) } let(:sha) { sample_commit.id } diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb index f63816fd92a..8a66b55cc15 100644 --- a/spec/models/ci/artifact_blob_spec.rb +++ b/spec/models/ci/artifact_blob_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe Ci::ArtifactBlob do - set(:project) { create(:project, :public) } - set(:build) { create(:ci_build, :artifacts, project: project) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:build) { create(:ci_build, :artifacts, project: project) } let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/another-subdirectory/banana_sample.gif') } subject { described_class.new(entry) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 013581c0d94..09d6d661d81 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -7,7 +7,7 @@ describe Ci::Pipeline, :mailer do include StubRequests let(:user) { create(:user) } - set(:project) { create(:project) } + let_it_be(:project) { create(:project) } let(:pipeline) do create(:ci_empty_pipeline, status: :created, project: project) @@ -231,7 +231,7 @@ describe Ci::Pipeline, :mailer do describe '#legacy_detached_merge_request_pipeline?' do subject { pipeline.legacy_detached_merge_request_pipeline? } - set(:merge_request) { create(:merge_request) } + let_it_be(:merge_request) { create(:merge_request) } let(:ref) { 'feature' } let(:target_sha) { nil } diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 44ca4a06e2d..85860da38ad 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -544,7 +544,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do end describe '#applications' do - set(:cluster) { create(:cluster) } + let_it_be(:cluster, reload: true) { create(:cluster) } subject { cluster.applications } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 782d1ac4552..c09f5bc4f4d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -17,7 +17,7 @@ describe Commit do end describe '.lazy' do - set(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } context 'when the commits are found' do let(:oids) do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 40652614101..e1a748da7fd 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' describe CommitStatus do - set(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } - set(:pipeline) do + let_it_be(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit.id) end diff --git a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb index 1fe90d3cc9a..d2373926802 100644 --- a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb +++ b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb @@ -15,7 +15,7 @@ describe BatchDestroyDependentAssociations do end describe '#dependent_associations_to_destroy' do - set(:project) { TestProject.new } + let_it_be(:project) { TestProject.new } it 'returns the right associations' do expect(project.dependent_associations_to_destroy.map(&:name)).to match_array([:builds]) @@ -23,9 +23,9 @@ describe BatchDestroyDependentAssociations do end describe '#destroy_dependent_associations_in_batches' do - set(:project) { create(:project) } - set(:build) { create(:ci_build, project: project) } - set(:notification_setting) { create(:notification_setting, project: project) } + let_it_be(:project) { create(:project) } + let_it_be(:build) { create(:ci_build, project: project) } + let_it_be(:notification_setting) { create(:notification_setting, project: project) } let!(:todos) { create(:todo, project: project) } it 'destroys multiple builds' do diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb index 74ddc2d6284..9f120775a3c 100644 --- a/spec/models/identity_spec.rb +++ b/spec/models/identity_spec.rb @@ -13,7 +13,7 @@ describe Identity do end describe 'validations' do - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } context 'with existing user and provider' do before do diff --git a/spec/models/label_note_spec.rb b/spec/models/label_note_spec.rb index dd2c702a7a9..34560acfa9e 100644 --- a/spec/models/label_note_spec.rb +++ b/spec/models/label_note_spec.rb @@ -3,20 +3,20 @@ require 'spec_helper' describe LabelNote do - set(:project) { create(:project, :repository) } - set(:user) { create(:user) } - set(:label) { create(:label, project: project) } - set(:label2) { create(:label, project: project) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:label) { create(:label, project: project) } + let_it_be(:label2) { create(:label, project: project) } let(:resource_parent) { project } context 'when resource is issue' do - set(:resource) { create(:issue, project: project) } + let_it_be(:resource) { create(:issue, project: project) } it_behaves_like 'label note created from events' end context 'when resource is merge request' do - set(:resource) { create(:merge_request, source_project: project, target_project: project) } + let_it_be(:resource) { create(:merge_request, source_project: project, target_project: project) } it_behaves_like 'label note created from events' end diff --git a/spec/models/lfs_file_lock_spec.rb b/spec/models/lfs_file_lock_spec.rb index a42346c341d..0a47ded43fb 100644 --- a/spec/models/lfs_file_lock_spec.rb +++ b/spec/models/lfs_file_lock_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe LfsFileLock do - set(:lfs_file_lock) { create(:lfs_file_lock) } + let_it_be(:lfs_file_lock, reload: true) { create(:lfs_file_lock) } subject { lfs_file_lock } it { is_expected.to belong_to(:project) } diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb index 44445429d3e..51713906d06 100644 --- a/spec/models/lfs_object_spec.rb +++ b/spec/models/lfs_object_spec.rb @@ -44,8 +44,8 @@ describe LfsObject do end describe '#project_allowed_access?' do - set(:lfs_object) { create(:lfs_objects_project).lfs_object } - set(:project) { create(:project) } + let_it_be(:lfs_object) { create(:lfs_objects_project).lfs_object } + let_it_be(:project, reload: true) { create(:project) } it 'returns true when project is linked' do create(:lfs_objects_project, lfs_object: lfs_object, project: project) @@ -58,9 +58,9 @@ describe LfsObject do end context 'when project is a member of a fork network' do - set(:fork_network) { create(:fork_network) } - set(:fork_network_root_project) { fork_network.root_project } - set(:fork_network_membership) { create(:fork_network_member, project: project, fork_network: fork_network) } + let_it_be(:fork_network) { create(:fork_network) } + let_it_be(:fork_network_root_project, reload: true) { fork_network.root_project } + let_it_be(:fork_network_membership) { create(:fork_network_member, project: project, fork_network: fork_network) } it 'returns true for all members when forked project is linked' do create(:lfs_objects_project, lfs_object: lfs_object, project: project) diff --git a/spec/models/lfs_objects_project_spec.rb b/spec/models/lfs_objects_project_spec.rb index e320f873989..31300828a43 100644 --- a/spec/models/lfs_objects_project_spec.rb +++ b/spec/models/lfs_objects_project_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe LfsObjectsProject do - set(:project) { create(:project) } + let_it_be(:project) { create(:project) } subject do create(:lfs_objects_project, project: project) diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 78b9e8bc217..8167241faa8 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -54,20 +54,20 @@ describe MergeRequestDiff do end describe '.ids_for_external_storage_migration' do - set(:merge_request) { create(:merge_request) } - set(:outdated) { merge_request.merge_request_diff } - set(:latest) { merge_request.create_merge_request_diff } + let_it_be(:merge_request) { create(:merge_request) } + let_it_be(:outdated) { merge_request.merge_request_diff } + let_it_be(:latest) { merge_request.create_merge_request_diff } - set(:closed_mr) { create(:merge_request, :closed_last_month) } + let_it_be(:closed_mr) { create(:merge_request, :closed_last_month) } let(:closed) { closed_mr.merge_request_diff } - set(:merged_mr) { create(:merge_request, :merged_last_month) } + let_it_be(:merged_mr) { create(:merge_request, :merged_last_month) } let(:merged) { merged_mr.merge_request_diff } - set(:recently_closed_mr) { create(:merge_request, :closed) } + let_it_be(:recently_closed_mr) { create(:merge_request, :closed) } let(:closed_recently) { recently_closed_mr.merge_request_diff } - set(:recently_merged_mr) { create(:merge_request, :merged) } + let_it_be(:recently_merged_mr) { create(:merge_request, :merged) } let(:merged_recently) { recently_merged_mr.merge_request_diff } before do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 470b67afe07..08348e3767a 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1091,8 +1091,8 @@ describe MergeRequest do end describe '#can_remove_source_branch?' do - set(:user) { create(:user) } - set(:merge_request) { create(:merge_request, :simple) } + let_it_be(:user) { create(:user) } + let_it_be(:merge_request, reload: true) { create(:merge_request, :simple) } subject { merge_request } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index d84a8665dc8..04587ef4240 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -197,6 +197,15 @@ describe Milestone do end end + it_behaves_like 'within_timeframe scope' do + let_it_be(:now) { Time.now } + let_it_be(:project) { create(:project, :empty_repo) } + let_it_be(:resource_1) { create(:milestone, project: project, start_date: now - 1.day, due_date: now + 1.day) } + let_it_be(:resource_2) { create(:milestone, project: project, start_date: now + 2.days, due_date: now + 3.days) } + let_it_be(:resource_3) { create(:milestone, project: project, due_date: now) } + let_it_be(:resource_4) { create(:milestone, project: project, start_date: now) } + end + describe "#percent_complete" do it "does not count open issues" do milestone.issues << issue @@ -517,9 +526,9 @@ describe Milestone do end describe '.sort_by_attribute' do - set(:milestone_1) { create(:milestone, title: 'Foo') } - set(:milestone_2) { create(:milestone, title: 'Bar') } - set(:milestone_3) { create(:milestone, title: 'Zoo') } + let_it_be(:milestone_1) { create(:milestone, title: 'Foo') } + let_it_be(:milestone_2) { create(:milestone, title: 'Bar') } + let_it_be(:milestone_3) { create(:milestone, title: 'Zoo') } context 'ordering by name ascending' do it 'sorts by title ascending' do @@ -555,7 +564,7 @@ describe Milestone do end it 'returns the quantity of milestones in each possible state' do - expected_count = { opened: 5, closed: 6, all: 11 } + expected_count = { opened: 2, closed: 6, all: 8 } count = described_class.states_count(Project.all, Group.all) expect(count).to eq(expected_count) diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb index 2a821b20aa8..5af25ac1437 100644 --- a/spec/models/project_auto_devops_spec.rb +++ b/spec/models/project_auto_devops_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ProjectAutoDevops do - set(:project) { build(:project) } + let_it_be(:project) { build(:project) } it_behaves_like 'having unique enum values' diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 679e6142416..fdacfbe24d1 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -40,7 +40,7 @@ describe Repository do end describe '#branch_names_contains' do - set(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } let(:repository) { project.repository } subject { repository.branch_names_contains(sample_commit.id) } @@ -328,7 +328,7 @@ describe Repository do end describe '#new_commits' do - set(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } let(:repository) { project.repository } subject { repository.new_commits(rev) } @@ -356,7 +356,7 @@ describe Repository do end describe '#commits_by' do - set(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } let(:oids) { TestEnv::BRANCH_SHA.values } subject { project.repository.commits_by(oids: oids) } @@ -2575,7 +2575,7 @@ describe Repository do end describe 'commit cache' do - set(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } it 'caches based on SHA' do # Gets the commit oid, and warms the cache @@ -2723,7 +2723,7 @@ describe Repository do end describe '#merge_base' do - set(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } subject(:repository) { project.repository } it 'only makes one gitaly call' do @@ -2782,7 +2782,7 @@ describe Repository do end describe "#blobs_metadata" do - set(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } let(:repository) { project.repository } def expect_metadata_blob(thing) diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb index 7539bf1e957..fedaae372c4 100644 --- a/spec/models/sent_notification_spec.rb +++ b/spec/models/sent_notification_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe SentNotification do - set(:user) { create(:user) } - set(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } describe 'validation' do describe 'note validity' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index d14de7e99de..f2b95e00b5e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2196,7 +2196,7 @@ describe User, :do_not_mock_admin_mode do describe '.find_by_private_commit_email' do context 'with email' do - set(:user) { create(:user) } + let_it_be(:user) { create(:user) } it 'returns user through private commit email' do expect(described_class.find_by_private_commit_email(user.private_commit_email)).to eq(user) diff --git a/spec/presenters/milestone_presenter_spec.rb b/spec/presenters/milestone_presenter_spec.rb new file mode 100644 index 00000000000..3d7b3ad6d78 --- /dev/null +++ b/spec/presenters/milestone_presenter_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MilestonePresenter do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:milestone) { create(:milestone, group: group) } + let_it_be(:presenter) { described_class.new(milestone, current_user: user) } + + before do + group.add_developer(user) + end + + describe '#milestone_path' do + it 'returns correct path' do + expect(presenter.milestone_path).to eq("/groups/#{group.full_path}/-/milestones/#{milestone.iid}") + end + end +end diff --git a/spec/requests/api/graphql/group/milestones_spec.rb b/spec/requests/api/graphql/group/milestones_spec.rb new file mode 100644 index 00000000000..84b14470fd1 --- /dev/null +++ b/spec/requests/api/graphql/group/milestones_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Milestones through GroupQuery' do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:now) { Time.now } + let_it_be(:group) { create(:group, :private) } + let_it_be(:milestone_1) { create(:milestone, group: group) } + let_it_be(:milestone_2) { create(:milestone, group: group, state: :closed, start_date: now, due_date: now + 1.day) } + let_it_be(:milestone_3) { create(:milestone, group: group, start_date: now, due_date: now + 2.days) } + let_it_be(:milestone_4) { create(:milestone, group: group, state: :closed, start_date: now - 2.days, due_date: now - 1.day) } + let_it_be(:milestone_from_other_group) { create(:milestone, group: create(:group)) } + + let(:milestone_data) { graphql_data['group']['milestones']['edges'] } + + describe 'Get list of milestones from a group' do + before do + group.add_developer(user) + end + + context 'when the request is correct' do + before do + fetch_milestones(user) + end + + it_behaves_like 'a working graphql query' + + it 'returns milestones successfully' do + expect(response).to have_gitlab_http_status(200) + expect(graphql_errors).to be_nil + expect_array_response(milestone_1.to_global_id.to_s, milestone_2.to_global_id.to_s, milestone_3.to_global_id.to_s, milestone_4.to_global_id.to_s) + end + end + + context 'when filtering by timeframe' do + it 'fetches milestones between start_date and due_date' do + fetch_milestones(user, { start_date: now.to_s, end_date: (now + 2.days).to_s }) + + expect_array_response(milestone_2.to_global_id.to_s, milestone_3.to_global_id.to_s) + end + end + + context 'when filtering by state' do + it 'returns milestones with given state' do + fetch_milestones(user, { state: :active }) + + expect_array_response(milestone_1.to_global_id.to_s, milestone_3.to_global_id.to_s) + end + end + + def fetch_milestones(user = nil, args = {}) + post_graphql(milestones_query(args), current_user: user) + end + + def milestones_query(args = {}) + milestone_node = <<~NODE + edges { + node { + id + title + state + } + } + NODE + + graphql_query_for("group", + { full_path: group.full_path }, + [query_graphql_field("milestones", args, milestone_node)] + ) + end + + def expect_array_response(*items) + expect(response).to have_gitlab_http_status(:success) + expect(milestone_data).to be_an Array + expect(milestone_node_array('id')).to match_array(items) + end + + def milestone_node_array(extract_attribute = nil) + node_array(milestone_data, extract_attribute) + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index e88209081d4..2b9ec4319f5 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1402,6 +1402,7 @@ describe API::Projects do expect(json_response['merge_requests_access_level']).to be_present expect(json_response['wiki_access_level']).to be_present expect(json_response['builds_access_level']).to be_present + expect(json_response).to have_key('emails_disabled') expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) expect(json_response['remove_source_branch_after_merge']).to be_truthy expect(json_response['container_registry_enabled']).to be_present @@ -1412,18 +1413,18 @@ describe API::Projects do expect(json_response['namespace']).to be_present expect(json_response['import_status']).to be_present expect(json_response).to include("import_error") - expect(json_response['avatar_url']).to be_nil + expect(json_response).to have_key('avatar_url') expect(json_response['star_count']).to be_present expect(json_response['forks_count']).to be_present expect(json_response['public_jobs']).to be_present - expect(json_response['ci_config_path']).to be_nil + expect(json_response).to have_key('ci_config_path') expect(json_response['shared_with_groups']).to be_an Array expect(json_response['shared_with_groups'].length).to eq(1) expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) expect(json_response['shared_with_groups'][0]['group_full_path']).to eq(group.full_path) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) - expect(json_response['shared_with_groups'][0]['expires_at']).to be_nil + expect(json_response['shared_with_groups'][0]).to have_key('expires_at') expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds) expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) expect(json_response['ci_default_git_depth']).to eq(project.ci_default_git_depth) @@ -2243,6 +2244,16 @@ describe API::Projects do expect(json_response['pages_access_level']).to eq('private') end + it 'updates emails_disabled' do + project_param = { emails_disabled: true } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['emails_disabled']).to eq(true) + end + it 'updates build_git_strategy' do project_param = { build_git_strategy: 'clone' } diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index e3ba366dfcc..23ce17b14fc 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -1607,7 +1607,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do it_behaves_like 'successful artifacts upload' end - context 'for file stored remotelly' do + context 'for file stored remotely' do let!(:fog_connection) do stub_artifacts_object_storage(direct_upload: true) end @@ -1894,6 +1894,46 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end + context 'when artifacts already exist for the job' do + let(:params) do + { + artifact_type: :archive, + artifact_format: :zip, + 'file.sha256' => uploaded_sha256 + } + end + + let(:existing_sha256) { '0' * 64 } + + let!(:existing_artifact) do + create(:ci_job_artifact, :archive, file_sha256: existing_sha256, job: job) + end + + context 'when sha256 is the same of the existing artifact' do + let(:uploaded_sha256) { existing_sha256 } + + it 'ignores the new artifact' do + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:created) + expect(job.reload.job_artifacts_archive).to eq(existing_artifact) + end + end + + context 'when sha256 is different than the existing artifact' do + let(:uploaded_sha256) { '1' * 64 } + + it 'logs and returns an error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception) + + upload_artifacts(file_upload, headers_with_token, params) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(job.reload.job_artifacts_archive).to eq(existing_artifact) + end + end + end + context 'when artifacts are being stored outside of tmp path' do let(:new_tmpdir) { Dir.mktmpdir } diff --git a/spec/services/ci/create_job_artifacts_service_spec.rb b/spec/services/ci/create_job_artifacts_service_spec.rb new file mode 100644 index 00000000000..e1146fc3df6 --- /dev/null +++ b/spec/services/ci/create_job_artifacts_service_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::CreateJobArtifactsService do + let(:service) { described_class.new } + let(:job) { create(:ci_build) } + let(:artifacts_sha256) { '0' * 64 } + let(:metadata_file) { nil } + + let(:artifacts_file) do + file_to_upload('spec/fixtures/ci_build_artifacts.zip', sha256: artifacts_sha256) + end + + let(:params) do + { + 'artifact_type' => 'archive', + 'artifact_format' => 'zip' + } + end + + def file_to_upload(path, params = {}) + upload = Tempfile.new('upload') + FileUtils.copy(path, upload.path) + + UploadedFile.new(upload.path, params) + end + + describe '#execute' do + subject { service.execute(job, artifacts_file, params, metadata_file: metadata_file) } + + context 'when artifacts file is uploaded' do + it 'saves artifact for the given type' do + expect { subject }.to change { Ci::JobArtifact.count }.by(1) + + new_artifact = job.job_artifacts.last + expect(new_artifact.project).to eq(job.project) + expect(new_artifact.file).to be_present + expect(new_artifact.file_type).to eq(params['artifact_type']) + expect(new_artifact.file_format).to eq(params['artifact_format']) + expect(new_artifact.file_sha256).to eq(artifacts_sha256) + end + + context 'when metadata file is also uploaded' do + let(:metadata_file) do + file_to_upload('spec/fixtures/ci_build_artifacts_metadata.gz', sha256: artifacts_sha256) + end + + before do + stub_application_setting(default_artifacts_expire_in: '1 day') + end + + it 'saves metadata artifact' do + expect { subject }.to change { Ci::JobArtifact.count }.by(2) + + new_artifact = job.job_artifacts.last + expect(new_artifact.project).to eq(job.project) + expect(new_artifact.file).to be_present + expect(new_artifact.file_type).to eq('metadata') + expect(new_artifact.file_format).to eq('gzip') + expect(new_artifact.file_sha256).to eq(artifacts_sha256) + end + + it 'sets expiration date according to application settings' do + expected_expire_at = 1.day.from_now + + expect(subject).to be_truthy + archive_artifact, metadata_artifact = job.job_artifacts.last(2) + + expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at) + expect(archive_artifact.expire_at).to be_within(1.minute).of(expected_expire_at) + expect(metadata_artifact.expire_at).to be_within(1.minute).of(expected_expire_at) + end + + context 'when expire_in params is set' do + before do + params.merge!('expire_in' => '2 hours') + end + + it 'sets expiration date according to the parameter' do + expected_expire_at = 2.hours.from_now + + expect(subject).to be_truthy + archive_artifact, metadata_artifact = job.job_artifacts.last(2) + + expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at) + expect(archive_artifact.expire_at).to be_within(1.minute).of(expected_expire_at) + expect(metadata_artifact.expire_at).to be_within(1.minute).of(expected_expire_at) + end + end + end + end + + context 'when artifacts file already exists' do + let!(:existing_artifact) do + create(:ci_job_artifact, :archive, file_sha256: existing_sha256, job: job) + end + + context 'when sha256 of uploading artifact is the same of the existing one' do + let(:existing_sha256) { artifacts_sha256 } + + it 'ignores the changes' do + expect { subject }.not_to change { Ci::JobArtifact.count } + expect(subject).to be_truthy + end + end + + context 'when sha256 of uploading artifact is different than the existing one' do + let(:existing_sha256) { '1' * 64 } + + it 'returns false and logs the error' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original + + expect { subject }.not_to change { Ci::JobArtifact.count } + expect(subject).to be_falsey + expect(job.errors[:base]).to contain_exactly('another artifact of the same type already exists') + end + end + end + end +end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 8dc99e4e042..35b1b802f35 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -185,12 +185,13 @@ module GraphqlHelpers end # Fairly dumb Ruby => GraphQL rendering function. Only suitable for testing. - # Missing support for Enums (feel free to add if you need it). + # Use symbol for Enum values def as_graphql_literal(value) case value when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]" when Integer, Float then value.to_s when String then "\"#{value.gsub(/"/, '\\"')}\"" + when Symbol then value when nil then 'null' when true then 'true' when false then 'false' diff --git a/spec/support/shared_examples/policies/within_timeframe_shared_examples.rb b/spec/support/shared_examples/policies/within_timeframe_shared_examples.rb new file mode 100644 index 00000000000..918db6886d3 --- /dev/null +++ b/spec/support/shared_examples/policies/within_timeframe_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'within_timeframe scope' do + describe '.within_timeframe' do + it 'returns resources with start_date and/or end_date between timeframe' do + resources = described_class.within_timeframe(now + 2.days, now + 3.days) + + expect(resources).to match_array([resource_2, resource_4]) + end + + it 'returns resources which starts before the timeframe' do + resources = described_class.within_timeframe(now, now + 1.day) + + expect(resources).to match_array([resource_1, resource_3, resource_4]) + end + + it 'returns resources which ends after the timeframe' do + resources = described_class.within_timeframe(now + 3.days, now + 5.days) + + expect(resources).to match_array([resource_2, resource_4]) + end + end +end diff --git a/yarn.lock b/yarn.lock index 5b8c5c84ce9..1c02c084bf7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -705,10 +705,10 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@gitlab/eslint-config@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-2.1.1.tgz#64fcc8135f1a6055181fd64b991e33eb43913153" - integrity sha512-+rQA+gIcZbkaQ7GIjDjfMnYz41fFtsEaF0cRmk0KSqXWTKmOi4gcYZppIPdRvJWKhNPRS735Y5Of3gdIObINYQ== +"@gitlab/eslint-config@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-2.1.2.tgz#9f4011d3bf15f3e2668a1faa754f0b9804f23f8f" + integrity sha512-+9yd5PKyipUVngEtKOdBxq7C6tXsUNdaGVD+SLBDqX0VaCNxQVWJvmQ2FPxb9gOLZsSAnP5Yl2Rj7dY0fJV4Gw== dependencies: "@gitlab/eslint-plugin-i18n" "^1.1.0" "@gitlab/eslint-plugin-vue-i18n" "^1.2.0" @@ -718,7 +718,6 @@ eslint-plugin-babel "^5.3.0" eslint-plugin-filenames "^1.3.2" eslint-plugin-import "^2.20.0" - eslint-plugin-no-jquery "^2.3.1" eslint-plugin-promise "^4.2.1" eslint-plugin-vue "^5.2.3" @@ -4350,7 +4349,7 @@ eslint-plugin-jest@^22.3.0: resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.3.0.tgz#a10f10dedfc92def774ec9bb5bfbd2fb8e1c96d2" integrity sha512-P1mYVRNlOEoO5T9yTqOfucjOYf1ktmJ26NjwjH8sxpCFQa6IhBGr5TpKl3hcAAT29hOsRJVuMWmTsHoUVo9FoA== -eslint-plugin-no-jquery@^2.3.0, eslint-plugin-no-jquery@^2.3.1: +eslint-plugin-no-jquery@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.3.1.tgz#1c364cb863a38cc1570c8020155b6004cca62178" integrity sha512-/fiQUBSOMUETnfBuiK5ewvtRbek1IRTy5ov/6RZ6nlybvZ337vyGaNPWM1KgaIoIeN7dairNrPfq0h7A0tpT3A==