Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-11 21:09:00 +00:00
parent 559d99e402
commit bf217da41b
73 changed files with 1382 additions and 504 deletions

View file

@ -2505,26 +2505,6 @@ Style/FrozenStringLiteralComment:
- 'qa/qa/fixtures/auto_devops_rack/Rakefile'
- 'qa/qa/fixtures/auto_devops_rack/config.ru'
- 'qa/qa/page/page_concern.rb'
- 'rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers.rb'
- 'rubocop/cop/gitlab/finder_with_find_by.rb'
- 'rubocop/cop/gitlab/keys-first-and-values-first.rb'
- 'rubocop/cop/gitlab/module_with_instance_variables.rb'
- 'rubocop/cop/gitlab/predicate_memoization.rb'
- 'rubocop/cop/migration/add_concurrent_foreign_key.rb'
- 'rubocop/cop/migration/add_concurrent_index.rb'
- 'rubocop/cop/migration/add_index.rb'
- 'rubocop/cop/migration/add_timestamps.rb'
- 'rubocop/cop/migration/datetime.rb'
- 'rubocop/cop/migration/hash_index.rb'
- 'rubocop/cop/migration/remove_column.rb'
- 'rubocop/cop/migration/remove_concurrent_index.rb'
- 'rubocop/cop/migration/remove_index.rb'
- 'rubocop/cop/migration/safer_boolean_column.rb'
- 'rubocop/cop/migration/timestamps.rb'
- 'rubocop/cop/migration/update_column_in_batches.rb'
- 'rubocop/cop/project_path_helper.rb'
- 'rubocop/migration_helpers.rb'
- 'rubocop/qa_helpers.rb'
- 'scripts/flaky_examples/detect-new-flaky-examples'
- 'scripts/flaky_examples/prune-old-flaky-examples'
- 'scripts/gather-test-memory-data'

View file

@ -1 +1 @@
d0a79053ba4fef55b59543b99327fc89aed64876
8cdbdb46b4fa31e0c2f1e2646baaf0ffb271b3a0

View file

@ -85,7 +85,7 @@ export default {
<gl-link
:href="action.path"
:data-method="action.method"
class="btn btn-primary"
class="btn gl-button btn-confirm gl-text-decoration-none!"
data-testid="job-empty-state-action"
>{{ action.button_title }}</gl-link
>

View file

@ -46,9 +46,9 @@ export default {
...mapGetters(['hasForwardDeploymentFailure']),
...mapState(['job', 'stages', 'jobs', 'selectedStage']),
retryButtonClass() {
let className = 'btn btn-retry';
let className = 'btn gl-button gl-text-decoration-none!';
className +=
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
this.job.status && this.job.recoverable ? ' btn-confirm' : ' btn-confirm-secondary';
return className;
},
hasArtifact() {
@ -94,7 +94,7 @@ export default {
<gl-link
v-if="job.cancel_path"
:href="job.cancel_path"
class="btn btn-default"
class="btn gl-button btn-default gl-text-decoration-none!"
data-method="post"
data-testid="cancel-button"
rel="nofollow"
@ -115,7 +115,7 @@ export default {
<gl-link
v-if="job.new_issue_path"
:href="job.new_issue_path"
class="btn btn-success btn-inverted float-left mr-2"
class="btn gl-button btn-success-secondary float-left mr-2 gl-text-decoration-none!"
data-testid="job-new-issue"
>{{ $options.i18n.newIssue }}
</gl-link>

View file

@ -30,10 +30,6 @@ export default {
type: String,
required: true,
},
statsUrl: {
type: String,
required: true,
},
},
detailedMetrics: [
{
@ -173,9 +169,6 @@ export default {
class="ml-auto"
@change-current-request="changeCurrentRequest"
/>
<div v-if="statsUrl" id="peek-stats" class="view">
<a class="gl-text-blue-300" :href="statsUrl">{{ s__('PerformanceBar|Stats') }}</a>
</div>
</div>
</div>
</template>

View file

@ -30,7 +30,6 @@ const initPerformanceBar = (el) => {
requestId: performanceBarData.requestId,
peekUrl: performanceBarData.peekUrl,
profileUrl: performanceBarData.profileUrl,
statsUrl: performanceBarData.statsUrl,
};
},
mounted() {
@ -121,7 +120,6 @@ const initPerformanceBar = (el) => {
requestId: this.requestId,
peekUrl: this.peekUrl,
profileUrl: this.profileUrl,
statsUrl: this.statsUrl,
},
on: {
'add-request': this.addRequestManually,

View file

@ -12,11 +12,6 @@ import PipelineUrl from './pipeline_url.vue';
import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelinesTimeago from './time_ago.vue';
/**
* Pipeline table row.
*
* Given the received object renders a table row in the pipelines' table.
*/
export default {
i18n: {
cancelTitle: __('Cancel'),
@ -127,116 +122,30 @@ export default {
return commitAuthorInformation;
},
/**
* If provided, returns the commit tag.
* Needed to render the commit component column.
*
* @returns {String|Undefined}
*/
commitTag() {
if (this.pipeline.ref && this.pipeline.ref.tag) {
return this.pipeline.ref.tag;
}
return undefined;
return this.pipeline?.ref?.tag;
},
/**
* If provided, returns the commit ref.
* Needed to render the commit component column.
*
* Matches `path` prop sent in the API to `ref_url` prop needed
* in the commit component.
*
* @returns {Object|Undefined}
*/
commitRef() {
if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'path') {
accumulator.ref_url = this.pipeline.ref[prop];
} else {
accumulator[prop] = this.pipeline.ref[prop];
}
return accumulator;
}, {});
}
return undefined;
return this.pipeline?.ref;
},
/**
* If provided, returns the commit url.
* Needed to render the commit component column.
*
* @returns {String|Undefined}
*/
commitUrl() {
if (this.pipeline.commit && this.pipeline.commit.commit_path) {
return this.pipeline.commit.commit_path;
}
return undefined;
return this.pipeline?.commit?.commit_path;
},
/**
* If provided, returns the commit short sha.
* Needed to render the commit component column.
*
* @returns {String|Undefined}
*/
commitShortSha() {
if (this.pipeline.commit && this.pipeline.commit.short_id) {
return this.pipeline.commit.short_id;
}
return undefined;
return this.pipeline?.commit?.short_id;
},
/**
* If provided, returns the commit title.
* Needed to render the commit component column.
*
* @returns {String|Undefined}
*/
commitTitle() {
if (this.pipeline.commit && this.pipeline.commit.title) {
return this.pipeline.commit.title;
}
return undefined;
return this.pipeline?.commit?.title;
},
/**
* Timeago components expects a number
*
* @return {type} description
*/
pipelineDuration() {
if (this.pipeline.details && this.pipeline.details.duration) {
return this.pipeline.details.duration;
}
return 0;
return this.pipeline?.details?.duration ?? 0;
},
/**
* Timeago component expects a String.
*
* @return {String}
*/
pipelineFinishedAt() {
if (this.pipeline.details && this.pipeline.details.finished_at) {
return this.pipeline.details.finished_at;
}
return '';
return this.pipeline?.details?.finished_at ?? '';
},
pipelineStatus() {
if (this.pipeline.details && this.pipeline.details.status) {
return this.pipeline.details.status;
}
return {};
return this.pipeline?.details?.status ?? {};
},
displayPipelineActions() {
return (
this.pipeline.flags.retryable ||
@ -245,11 +154,9 @@ export default {
this.pipeline.details.artifacts.length
);
},
isChildView() {
return this.viewType === 'child';
},
isCancelling() {
return this.cancelingPipeline === this.pipeline.id;
},
@ -355,7 +262,7 @@ export default {
:title="$options.i18n.redeployTitle"
:disabled="isRetrying"
:loading="isRetrying"
class="js-pipelines-retry-button btn-retry"
class="js-pipelines-retry-button"
data-qa-selector="pipeline_retry_button"
icon="repeat"
variant="default"

View file

@ -133,6 +133,9 @@ export default {
? sprintf(__("%{username}'s avatar"), { username: this.author.username })
: null;
},
refUrl() {
return this.commitRef.ref_url || this.commitRef.path;
},
},
};
</script>
@ -156,9 +159,10 @@ export default {
<gl-link
v-else
v-gl-tooltip
:href="commitRef.ref_url"
:href="refUrl"
:title="commitRef.name"
class="ref-name"
data-testid="ref-name"
>{{ commitRef.name }}</gl-link
>
</template>

View file

@ -84,7 +84,16 @@ class HelpController < ApplicationController
end
def documentation_base_url
@documentation_base_url ||= Gitlab::CurrentSettings.current_application_settings.help_page_documentation_base_url.presence
@documentation_base_url ||= documentation_base_url_from_yml_configuration || documentation_base_url_from_db
end
# DEPRECATED
def documentation_base_url_from_db
Gitlab::CurrentSettings.current_application_settings.help_page_documentation_base_url.presence
end
def documentation_base_url_from_yml_configuration
::Gitlab.config.gitlab_docs.host.presence if ::Gitlab.config.gitlab_docs.enabled
end
def documentation_file_path

View file

@ -145,9 +145,6 @@ class Projects::IssuesController < Projects::ApplicationController
format.html do
recaptcha_check_with_fallback { render :new }
end
format.js do
@link = @issue.attachment.url.to_js
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Types
class EventActionEnum < BaseEnum
graphql_name 'EventAction'
description 'Event action'
::Event.actions.keys.each do |target_type|
value target_type.upcase, value: target_type, description: "#{target_type.titleize} action"
end
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Types
class EventType < BaseObject
graphql_name 'Event'
description 'Representing an event'
present_using EventPresenter
authorize :read_event
field :id, GraphQL::ID_TYPE,
description: 'ID of the event.',
null: false
field :author, Types::UserType,
description: 'Author of this event.',
null: false
field :action, Types::EventActionEnum,
description: 'Action of the event.',
null: false
field :created_at, Types::TimeType,
description: 'When this event was created.',
null: false
field :updated_at, Types::TimeType,
description: 'When this event was updated.',
null: false
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
end
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module Types
module EventableType
include Types::BaseInterface
field :events, Types::EventType.connection_type, null: true, description: 'A list of events associated with the object.'
end
end

View file

@ -10,6 +10,9 @@ module Enums
unknown_failure: 0,
config_error: 1,
external_validation_failure: 2,
activity_limit_exceeded: 20,
size_limit_exceeded: 21,
job_activity_limit_exceeded: 22,
deployments_limit_exceeded: 23
}
end
@ -77,5 +80,3 @@ module Enums
end
end
end
Enums::Ci::Pipeline.prepend_if_ee('EE::Enums::Ci::Pipeline')

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class EventPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
condition(:visible_to_user) do
subject.visible_to_user?(user)
end
rule { visible_to_user }.enable :read_event
end

View file

@ -2,10 +2,11 @@
module Pages
class MigrateFromLegacyStorageService
def initialize(logger, migration_threads, batch_size)
def initialize(logger, migration_threads:, batch_size:, ignore_invalid_entries:)
@logger = logger
@migration_threads = migration_threads
@batch_size = batch_size
@ignore_invalid_entries = ignore_invalid_entries
@migrated = 0
@errored = 0
@ -59,19 +60,19 @@ module Pages
def migrate_project(project)
result = nil
time = Benchmark.realtime do
result = ::Pages::MigrateLegacyStorageToDeploymentService.new(project).execute
result = ::Pages::MigrateLegacyStorageToDeploymentService.new(project, ignore_invalid_entries: @ignore_invalid_entries).execute
end
if result[:status] == :success
@logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time} seconds")
@logger.info("project_id: #{project.id} #{project.pages_path} has been migrated in #{time.round(2)} seconds")
@counters_lock.synchronize { @migrated += 1 }
else
@logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time} seconds: #{result[:message]}")
@logger.error("project_id: #{project.id} #{project.pages_path} failed to be migrated in #{time.round(2)} seconds: #{result[:message]}")
@counters_lock.synchronize { @errored += 1 }
end
rescue => e
@counters_lock.synchronize { @errored += 1 }
@logger.error("#{e.message} project_id: #{project&.id}")
@logger.error("project_id: #{project&.id} #{project&.pages_path} failed to be migrated: #{e.message}")
Gitlab::ErrorTracking.track_exception(e, project_id: project&.id)
end
end

View file

@ -9,8 +9,9 @@ module Pages
attr_reader :project
def initialize(project)
def initialize(project, ignore_invalid_entries: false)
@project = project
@ignore_invalid_entries = ignore_invalid_entries
end
def execute
@ -26,7 +27,7 @@ module Pages
private
def execute_unsafe
zip_result = ::Pages::ZipDirectoryService.new(project.pages_path).execute
zip_result = ::Pages::ZipDirectoryService.new(project.pages_path, ignore_invalid_entries: @ignore_invalid_entries).execute
if zip_result[:status] == :error
if !project.pages_metadatum&.reload&.pages_deployment &&

View file

@ -10,12 +10,17 @@ module Pages
PUBLIC_DIR = 'public'
def initialize(input_dir)
attr_reader :public_dir, :real_dir
def initialize(input_dir, ignore_invalid_entries: false)
@input_dir = input_dir
@ignore_invalid_entries = ignore_invalid_entries
end
def execute
return error("Can not find valid public dir in #{@input_dir}") unless valid_path?(public_dir)
unless resolve_public_dir
return error("Can not find valid public dir in #{@input_dir}")
end
output_file = File.join(real_dir, "@migrated.zip") # '@' to avoid any name collision with groups or projects
@ -35,24 +40,36 @@ module Pages
private
def resolve_public_dir
@real_dir = File.realpath(@input_dir)
@public_dir = File.join(real_dir, PUBLIC_DIR)
valid_path?(public_dir)
rescue Errno::ENOENT
false
end
def write_entry(zipfile, zipfile_path)
disk_file_path = File.join(real_dir, zipfile_path)
unless valid_path?(disk_file_path)
# archive with invalid entry will just have this entry missing
raise InvalidEntryError
raise InvalidEntryError, "#{disk_file_path} is invalid, input_dir: #{@input_dir}"
end
case File.lstat(disk_file_path).ftype
ftype = File.lstat(disk_file_path).ftype
case ftype
when 'directory'
recursively_zip_directory(zipfile, disk_file_path, zipfile_path)
when 'file', 'link'
zipfile.add(zipfile_path, disk_file_path)
else
raise InvalidEntryError
raise InvalidEntryError, "#{disk_file_path} has invalid ftype: #{ftype}, input_dir: #{@input_dir}"
end
rescue InvalidEntryError => e
rescue Errno::ENOENT, Errno::ELOOP, InvalidEntryError => e
Gitlab::ErrorTracking.track_exception(e, input_dir: @input_dir, disk_file_path: disk_file_path)
raise e unless @ignore_invalid_entries
end
def recursively_zip_directory(zipfile, disk_file_path, zipfile_path)
@ -70,31 +87,11 @@ module Pages
end
end
# that should never happen, but we want to be safer
# in theory without this we would allow to use symlinks
# to pack any directory on disk
# it isn't possible because SafeZip doesn't extract such archives
# SafeZip was introduced only recently,
# so we have invalid entries on disk
def valid_path?(disk_file_path)
realpath = File.realpath(disk_file_path)
realpath == public_dir || realpath.start_with?(public_dir + "/")
# happens if target of symlink isn't there
rescue => e
Gitlab::ErrorTracking.track_exception(e, input_dir: real_dir, disk_file_path: disk_file_path)
false
end
def real_dir
strong_memoize(:real_dir) do
File.realpath(@input_dir) rescue nil
end
end
def public_dir
strong_memoize(:public_dir) do
File.join(real_dir, PUBLIC_DIR) rescue nil
end
end
end
end

View file

@ -2,6 +2,5 @@
#js-peek{ data: { env: Peek.env,
request_id: peek_request_id,
stats_url: ENV.fetch('GITLAB_PERFORMANCE_BAR_STATS_URL', ''),
peek_url: "#{peek_routes_path}/results" },
class: Peek.env }

View file

@ -0,0 +1,5 @@
---
title: Apply new GitLab UI for buttons in pipeline page
merge_request: 53364
author: Yogi (@yo)
type: other

View file

@ -0,0 +1,5 @@
---
title: Add semgrep SAST analyzer
merge_request: 53815
author: Daniel Paul Searles
type: added

View file

@ -310,6 +310,13 @@ Settings.pages['secret_file'] ||= Rails.root.join('.gitlab_pages_secret')
Settings.pages['storage_path'] = Settings.pages['path']
Settings.pages['object_store'] = ObjectStoreSettings.legacy_parse(Settings.pages['object_store'])
#
# GitLab documentation
#
Settings['gitlab_docs'] ||= Settingslogic.new({})
Settings.gitlab_docs['enabled'] ||= false
Settings.gitlab_docs['host'] = nil unless Settings.gitlab_docs.enabled
#
# Geo
#

View file

@ -6,8 +6,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Performance Bar **(FREE SELF)**
> The **Stats** field [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/271551) in GitLab SaaS 13.9.
You can display the GitLab Performance Bar to see statistics for the performance
of a page. When activated, it looks as follows:
@ -55,8 +53,6 @@ From left to right, it displays:
- **Request Selector**: a select box displayed on the right-hand side of the
Performance Bar which enables you to view these metrics for any requests made while
the current page was open. Only the first two requests per unique URL are captured.
- **Stats** (optional): if the `GITLAB_PERFORMANCE_BAR_STATS_URL` environment variable is set,
this URL is displayed in the bar. In GitLab 13.9 and later, used only in GitLab SaaS.
## Request warnings

View file

@ -1639,7 +1639,7 @@ type BoardEdge {
"""
Represents an epic on an issue board
"""
type BoardEpic implements CurrentUserTodos & Noteable {
type BoardEpic implements CurrentUserTodos & Eventable & Noteable {
"""
Author of the epic.
"""
@ -1878,6 +1878,31 @@ type BoardEpic implements CurrentUserTodos & Noteable {
"""
dueDateIsFixed: Boolean
"""
A list of events associated with the object.
"""
events(
"""
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
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): EventConnection
"""
Group to which the epic belongs.
"""
@ -8682,7 +8707,7 @@ type EnvironmentsCanaryIngressUpdatePayload {
"""
Represents an epic
"""
type Epic implements CurrentUserTodos & Noteable {
type Epic implements CurrentUserTodos & Eventable & Noteable {
"""
Author of the epic.
"""
@ -8921,6 +8946,31 @@ type Epic implements CurrentUserTodos & Noteable {
"""
dueDateIsFixed: Boolean
"""
A list of events associated with the object.
"""
events(
"""
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
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): EventConnection
"""
Group to which the epic belongs.
"""
@ -10251,6 +10301,168 @@ enum EpicWildcardId {
NONE
}
"""
Representing an event
"""
type Event {
"""
Action of the event.
"""
action: EventAction!
"""
Author of this event.
"""
author: User!
"""
When this event was created.
"""
createdAt: Time!
"""
ID of the event.
"""
id: ID!
"""
When this event was updated.
"""
updatedAt: Time!
}
"""
Event action
"""
enum EventAction {
"""
Approved action
"""
APPROVED
"""
Archived action
"""
ARCHIVED
"""
Closed action
"""
CLOSED
"""
Commented action
"""
COMMENTED
"""
Created action
"""
CREATED
"""
Destroyed action
"""
DESTROYED
"""
Expired action
"""
EXPIRED
"""
Joined action
"""
JOINED
"""
Left action
"""
LEFT
"""
Merged action
"""
MERGED
"""
Pushed action
"""
PUSHED
"""
Reopened action
"""
REOPENED
"""
Updated action
"""
UPDATED
}
"""
The connection type for Event.
"""
type EventConnection {
"""
A list of edges.
"""
edges: [EventEdge]
"""
A list of nodes.
"""
nodes: [Event]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type EventEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Event
}
interface Eventable {
"""
A list of events associated with the object.
"""
events(
"""
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
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): EventConnection
}
"""
Autogenerated input type of ExportRequirements
"""

View file

@ -4855,6 +4855,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "events",
"description": "A list of events associated with the object.",
"args": [
{
"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": "EventConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "group",
"description": "Group to which the epic belongs.",
@ -5516,6 +5569,11 @@
"kind": "INTERFACE",
"name": "CurrentUserTodos",
"ofType": null
},
{
"kind": "INTERFACE",
"name": "Eventable",
"ofType": null
}
],
"enumValues": null,
@ -24566,6 +24624,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "events",
"description": "A list of events associated with the object.",
"args": [
{
"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": "EventConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "group",
"description": "Group to which the epic belongs.",
@ -25213,6 +25324,11 @@
"kind": "INTERFACE",
"name": "CurrentUserTodos",
"ofType": null
},
{
"kind": "INTERFACE",
"name": "Eventable",
"ofType": null
}
],
"enumValues": null,
@ -28276,6 +28392,385 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Event",
"description": "Representing an event",
"fields": [
{
"name": "action",
"description": "Action of the event.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "EventAction",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "author",
"description": "Author of this event.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "User",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "When this event was created.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the event.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updatedAt",
"description": "When this event was updated.",
"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": "ENUM",
"name": "EventAction",
"description": "Event action",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "CREATED",
"description": "Created action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UPDATED",
"description": "Updated action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "CLOSED",
"description": "Closed action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "REOPENED",
"description": "Reopened action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PUSHED",
"description": "Pushed action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "COMMENTED",
"description": "Commented action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "MERGED",
"description": "Merged action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "JOINED",
"description": "Joined action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "LEFT",
"description": "Left action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DESTROYED",
"description": "Destroyed action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "EXPIRED",
"description": "Expired action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "APPROVED",
"description": "Approved action",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ARCHIVED",
"description": "Archived action",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "EventConnection",
"description": "The connection type for Event.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "EventEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Event",
"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": "EventEdge",
"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": "Event",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INTERFACE",
"name": "Eventable",
"description": null,
"fields": [
{
"name": "events",
"description": "A list of events associated with the object.",
"args": [
{
"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": "EventConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": [
{
"kind": "OBJECT",
"name": "BoardEpic",
"ofType": null
},
{
"kind": "OBJECT",
"name": "Epic",
"ofType": null
}
]
},
{
"kind": "INPUT_OBJECT",
"name": "ExportRequirementsInput",

View file

@ -309,6 +309,7 @@ Represents an epic on an issue board.
| `dueDateFixed` | Time | Fixed due date of the epic. |
| `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones. |
| `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set. |
| `events` | EventConnection | A list of events associated with the object. |
| `group` | Group! | Group to which the epic belongs. |
| `hasChildren` | Boolean! | Indicates if the epic has children. |
| `hasIssues` | Boolean! | Indicates if the epic has direct issues. |
@ -1467,6 +1468,7 @@ Represents an epic.
| `dueDateFixed` | Time | Fixed due date of the epic. |
| `dueDateFromMilestones` | Time | Inherited due date of the epic from milestones. |
| `dueDateIsFixed` | Boolean | Indicates if the due date has been manually set. |
| `events` | EventConnection | A list of events associated with the object. |
| `group` | Group! | Group to which the epic belongs. |
| `hasChildren` | Boolean! | Indicates if the epic has children. |
| `hasIssues` | Boolean! | Indicates if the epic has direct issues. |
@ -1678,6 +1680,18 @@ Autogenerated return type of EpicTreeReorder.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### Event
Representing an event.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `action` | EventAction! | Action of the event. |
| `author` | User! | Author of this event. |
| `createdAt` | Time! | When this event was created. |
| `id` | ID! | ID of the event. |
| `updatedAt` | Time! | When this event was updated. |
### ExportRequirementsPayload
Autogenerated return type of ExportRequirements.
@ -4879,6 +4893,26 @@ Epic ID wildcard values.
| `ANY` | Any epic is assigned |
| `NONE` | No epic is assigned |
### EventAction
Event action.
| Value | Description |
| ----- | ----------- |
| `APPROVED` | Approved action |
| `ARCHIVED` | Archived action |
| `CLOSED` | Closed action |
| `COMMENTED` | Commented action |
| `CREATED` | Created action |
| `DESTROYED` | Destroyed action |
| `EXPIRED` | Expired action |
| `JOINED` | Joined action |
| `LEFT` | Left action |
| `MERGED` | Merged action |
| `PUSHED` | Pushed action |
| `REOPENED` | Reopened action |
| `UPDATED` | Updated action |
### GroupMemberRelation
Group member relation.

View file

@ -311,6 +311,9 @@ Custom event tracking and instrumentation can be added by directly calling the `
| `property` | String | nil | As described in [Structured event taxonomy](#structured-event-taxonomy). |
| `value` | Numeric | nil | As described in [Structured event taxonomy](#structured-event-taxonomy). |
| `context` | Array\[SelfDescribingJSON\] | nil | An array of custom contexts to send with this event. Most events should not have any custom contexts. |
| `project` | Project | nil | The project associated with the event |
| `user` | User | nil | The user associated with the event |
| `namespace` | Namespace | nil | The namespace associated with the event |
Tracking can be viewed as either tracking user behavior, or can be used for instrumentation to monitor and visualize performance over time in an area or aspect of code.
@ -321,10 +324,8 @@ class Projects::CreateService < BaseService
def execute
project = Project.create(params)
Gitlab::Tracking.event('Projects::CreateService', 'create_project',
label: project.errors.full_messages.to_sentence,
value: project.valid?
)
Gitlab::Tracking.event('Projects::CreateService', 'create_project', label: project.errors.full_messages.to_sentence,
property: project.valid?.to_s, project: project, user: current_user, namespace: namespace)
end
end
```

View file

@ -33,6 +33,7 @@ SAST supports the following official analyzers:
- [`phpcs-security-audit`](https://gitlab.com/gitlab-org/security-products/analyzers/phpcs-security-audit) (PHP CS security-audit)
- [`pmd-apex`](https://gitlab.com/gitlab-org/security-products/analyzers/pmd-apex) (PMD (Apex only))
- [`security-code-scan`](https://gitlab.com/gitlab-org/security-products/analyzers/security-code-scan) (Security Code Scan (.NET))
- [`semgrep`](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) (Semgrep)
- [`sobelow`](https://gitlab.com/gitlab-org/security-products/analyzers/sobelow) (Sobelow (Elixir Phoenix))
- [`spotbugs`](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs) (SpotBugs with the Find Sec Bugs plugin (Ant, Gradle and wrapper, Grails, Maven and wrapper, SBT))
@ -153,24 +154,24 @@ The [Security Scanner Integration](../../../development/integrations/secure.md)
## Analyzers Data
| Property / Tool | Apex | Bandit | Brakeman | ESLint security | SpotBugs | Flawfinder | Gosec | Kubesec Scanner | MobSF | NodeJsScan | PHP CS Security Audit | Security code Scan (.NET) | Sobelow |
| --------------------------------------- | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :---------------------: | :-------------------------: | :----------------: |
| Severity | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ |
| Title | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Description | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ |
| File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Start line | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ |
| End line | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Start column | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ |
| End column | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| External ID (for example, CVE) | ✗ | ✗ | ⚠ | ✗ | ⚠ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| URLs | ✓ | ✗ | ✓ | ✗ | ⚠ | ✗ | ⚠ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Internal doc/explanation | ✓ | ⚠ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Solution | ✓ | ✗ | ✗ | ✗ | ⚠ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Affected item (for example, class or package) | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Confidence | ✗ | ✓ | ✓ | ✗ | ✓ | x | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Source code extract | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ |
| Property / Tool | Apex | Bandit | Brakeman | ESLint security | SpotBugs | Flawfinder | Gosec | Kubesec Scanner | MobSF | NodeJsScan | PHP CS Security Audit | Security code Scan (.NET) | Semgrep | Sobelow |
| --------------------------------------- | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :------------------: | :---------------------: | :-------------------------: | :-------------------------: | :----------------: |
| Severity | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ⚠ | ✗ |
| Title | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Description | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ |
| File | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| Start line | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| End line | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Start column | ✓ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✗ |
| End column | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| External ID (for example, CVE) | ✗ | ✗ | ⚠ | ✗ | ⚠ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ⚠ | ✗ |
| URLs | ✓ | ✗ | ✓ | ✗ | ⚠ | ✗ | ⚠ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Internal doc/explanation | ✓ | ⚠ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Solution | ✓ | ✗ | ✗ | ✗ | ⚠ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ⚠ | ✗ |
| Affected item (for example, class or package) | ✓ | ✗ | ✓ | ✗ | ✓ | ✓ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Confidence | ✗ | ✓ | ✓ | ✗ | ✓ | x | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ⚠ | ✓ |
| Source code extract | ✗ | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ |
| Internal ID | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
- ✓ => we have that data
- ⚠ => we have that data but it's partially reliable, or we need to extract it from unstructured content

View file

@ -83,6 +83,7 @@ You can also [view our language roadmap](https://about.gitlab.com/direction/secu
| Objective-C (iOS) | [MobSF (beta)](https://github.com/MobSF/Mobile-Security-Framework-MobSF) | 13.5 |
| PHP | [phpcs-security-audit](https://github.com/FloeDesignTechnologies/phpcs-security-audit) | 10.8 |
| Python ([pip](https://pip.pypa.io/en/stable/)) | [bandit](https://github.com/PyCQA/bandit) | 10.3 |
| Python | [semgrep](https://semgrep.dev) | 13.9 |
| React | [ESLint react plugin](https://github.com/yannickcr/eslint-plugin-react) | 12.5 |
| Ruby on Rails | [brakeman](https://brakemanscanner.org) | 10.3 |
| Scala ([Ant](https://ant.apache.org/), [Gradle](https://gradle.org/), [Maven](https://maven.apache.org/), and [SBT](https://www.scala-sbt.org/)) | [SpotBugs](https://spotbugs.github.io/) with the [find-sec-bugs](https://find-sec-bugs.github.io/) plugin | 11.0 (SBT) & 11.9 (Ant, Gradle, Maven) |
@ -111,6 +112,7 @@ The following analyzers have multi-project support:
- MobSF
- PMD
- Security Code Scan
- Semgrep
- SpotBugs
- Sobelow
@ -681,20 +683,21 @@ For details on saving and transporting Docker images as a file, see Docker's doc
Support for custom certificate authorities was introduced in the following versions.
| Analyzer | Version |
| -------- | ------- |
| `bandit` | [v2.3.0](https://gitlab.com/gitlab-org/security-products/analyzers/bandit/-/releases/v2.3.0) |
| `brakeman` | [v2.1.0](https://gitlab.com/gitlab-org/security-products/analyzers/brakeman/-/releases/v2.1.0) |
| `eslint` | [v2.9.2](https://gitlab.com/gitlab-org/security-products/analyzers/eslint/-/releases/v2.9.2) |
| `flawfinder` | [v2.3.0](https://gitlab.com/gitlab-org/security-products/analyzers/flawfinder/-/releases/v2.3.0) |
| `gosec` | [v2.5.0](https://gitlab.com/gitlab-org/security-products/analyzers/gosec/-/releases/v2.5.0) |
| `kubesec` | [v2.1.0](https://gitlab.com/gitlab-org/security-products/analyzers/kubesec/-/releases/v2.1.0) |
| `nodejs-scan` | [v2.9.5](https://gitlab.com/gitlab-org/security-products/analyzers/nodejs-scan/-/releases/v2.9.5) |
| Analyzer | Version |
| -------- | ------- |
| `bandit` | [v2.3.0](https://gitlab.com/gitlab-org/security-products/analyzers/bandit/-/releases/v2.3.0) |
| `brakeman` | [v2.1.0](https://gitlab.com/gitlab-org/security-products/analyzers/brakeman/-/releases/v2.1.0) |
| `eslint` | [v2.9.2](https://gitlab.com/gitlab-org/security-products/analyzers/eslint/-/releases/v2.9.2) |
| `flawfinder` | [v2.3.0](https://gitlab.com/gitlab-org/security-products/analyzers/flawfinder/-/releases/v2.3.0) |
| `gosec` | [v2.5.0](https://gitlab.com/gitlab-org/security-products/analyzers/gosec/-/releases/v2.5.0) |
| `kubesec` | [v2.1.0](https://gitlab.com/gitlab-org/security-products/analyzers/kubesec/-/releases/v2.1.0) |
| `nodejs-scan` | [v2.9.5](https://gitlab.com/gitlab-org/security-products/analyzers/nodejs-scan/-/releases/v2.9.5) |
| `phpcs-security-audit` | [v2.8.2](https://gitlab.com/gitlab-org/security-products/analyzers/phpcs-security-audit/-/releases/v2.8.2) |
| `pmd-apex` | [v2.1.0](https://gitlab.com/gitlab-org/security-products/analyzers/pmd-apex/-/releases/v2.1.0) |
| `security-code-scan` | [v2.7.3](https://gitlab.com/gitlab-org/security-products/analyzers/security-code-scan/-/releases/v2.7.3) |
| `sobelow` | [v2.2.0](https://gitlab.com/gitlab-org/security-products/analyzers/sobelow/-/releases/v2.2.0) |
| `spotbugs` | [v2.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs/-/releases/v2.7.1) |
| `pmd-apex` | [v2.1.0](https://gitlab.com/gitlab-org/security-products/analyzers/pmd-apex/-/releases/v2.1.0) |
| `security-code-scan` | [v2.7.3](https://gitlab.com/gitlab-org/security-products/analyzers/security-code-scan/-/releases/v2.7.3) |
| `semgrep` | [v0.0.1](https://gitlab.com/gitlab-org/security-products/analyzers/security-code-scan/-/releases/v0.0.1) |
| `sobelow` | [v2.2.0](https://gitlab.com/gitlab-org/security-products/analyzers/sobelow/-/releases/v2.2.0) |
| `spotbugs` | [v2.7.1](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs/-/releases/v2.7.1) |
### Set SAST CI job variables to use local SAST analyzers

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

View file

@ -319,15 +319,24 @@ their own authors. To change this setting:
Note that users can edit the approval rules in every merge request and override pre-defined settings unless it's set [**not to allow** overrides](#prevent-overriding-default-approvals).
You can prevent authors from approving their own merge requests
[at the instance level](../../admin_area/merge_requests_approvals.md). When enabled,
this setting is disabled on the project level, and not editable.
#### Prevent approval of merge requests by their committers **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10441) in GitLab 11.10.
> - Moved to GitLab Premium in 13.9.
You can prevent users who have committed to a merge request from approving it,
though code authors can still approve. To enable this feature:
though code authors can still approve. You can enable this feature
[at the instance level](../../admin_area/merge_requests_approvals.md), which
disables changes to this feature at the project level. If you prefer to manage
this feature at the project level, you can:
1. Check the **Prevent MR approvals from users who make commits to the MR.** checkbox.
If this check box is disabled, this feature has been disabled
[at the instance level](../../admin_area/merge_requests_approvals.md).
1. Click **Save changes**.
Read the official Git documentation for an explanation of the

View file

@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Service Desk **(FREE)**
> - Moved to GitLab Free in 13.2.
> Moved to GitLab Free in 13.2.
Service Desk is a module that allows your team to connect
with any external party through email, without any external tools.
@ -54,36 +54,41 @@ Here's how Service Desk works for you:
## Configuring Service Desk
NOTE:
Service Desk is enabled on GitLab.com.
You can skip step 1 below; you only need to enable it per project.
Users with Maintainer and higher access in a project can configure Service Desk.
If you have project maintainer access you have the option to set up Service Desk. Follow these steps:
Service Desk issues are [confidential](issues/confidential_issues.md), so they are
only visible to project members. In GitLab 11.7 we updated the generated email
address format. The older format is still supported, so existing aliases or
contacts still work.
1. [Set up incoming email](../../administration/incoming_email.md#set-it-up) for the GitLab instance.
If you have [templates](description_templates.md) in your repository, you can optionally select
one from the selector menu to append it to all Service Desk issues.
To enable Service Desk in your project:
1. (GitLab self-managed only) [Set up incoming email](../../administration/incoming_email.md#set-it-up) for the GitLab instance.
We recommend using [email sub-addressing](../../administration/incoming_email.md#email-sub-addressing),
but in GitLab 11.7 and later you can also use [catch-all mailboxes](../../administration/incoming_email.md#catch-all-mailbox).
1. Navigate to your project's **Settings > General** and locate the **Service Desk** section.
but you can also use [catch-all mailboxes](../../administration/incoming_email.md#catch-all-mailbox).
1. In a project, in the left sidebar, go to **Settings > General** and expand the **Service Desk** section.
1. Enable the **Activate Service Desk** toggle. This reveals a unique email address to email issues
to the project. These issues are [confidential](issues/confidential_issues.md), so they are
only visible to project members. Note that in GitLab 11.7, we updated the generated email
address's format. The older format is still supported, however, allowing existing aliases or
contacts to continue working.
to the project.
WARNING:
This email address can be used by anyone to create an issue on this project, regardless
of their access level to your GitLab instance. We recommend **putting this behind an alias on your email system** so it can be
changed if needed. We also recommend **[enabling Akismet](../../integration/akismet.md)** on your GitLab
instance to add spam checking to this service. Unblocked email spam would result in many spam
issues being created.
Service Desk is now enabled for this project! To access it in a project, in the left sidebar, select
**Issues > Service Desk**.
If you have [templates](description_templates.md) in your repository, you can optionally select
one from the selector menu to append it to all Service Desk issues.
WARNING:
Anyone in your project can use the Service Desk email address to create an issue in this project, **regardless
of their access level** to your GitLab instance.
Service Desk is now enabled for this project! You should be able to access it from your project's
**Issues** menu.
To improve your project's security, we recommend the following:
![Service Desk Navigation Item](img/service_desk_nav_item.png)
- Put the Service Desk email address behind an alias on your email system so you can change it later.
- [Enable Akismet](../../integration/akismet.md) on your GitLab instance to add spam checking to this service.
Unblocked email spam can result in many spam issues being created.
The unique internal email address is visible to all project members in your GitLab instance.
However, when using an email alias externally, an end user (issue creator) cannot see the internal
email address displayed in the information note.
### Using customized email templates
@ -232,7 +237,8 @@ The configuration options are the same as for configuring
## Using Service Desk
There are a few ways Service Desk can be used.
You can use Service Desk to [create an issue](#as-an-end-user-issue-creator) or [respond to one](#as-a-responder-to-the-issue).
In these issues, you can also see our friendly neighborhood [Support Bot](#support-bot-user).
### As an end user (issue creator)

View file

@ -1,7 +1,7 @@
---
type: howto
stage: Fulfillment
group: Provision
group: Utilization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---

View file

@ -9,7 +9,7 @@ variables:
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, sobelow, pmd-apex, kubesec, mobsf"
SAST_DEFAULT_ANALYZERS: "bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, sobelow, pmd-apex, kubesec, mobsf, semgrep"
SAST_EXCLUDED_ANALYZERS: ""
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"
SAST_ANALYZER_IMAGE_TAG: 2
@ -244,6 +244,23 @@ security-code-scan-sast:
- '**/*.csproj'
- '**/*.vbproj'
semgrep-sast:
extends: .sast-analyzer
image:
name: "$SAST_ANALYZER_IMAGE"
variables:
SAST_ANALYZER_IMAGE: "$SECURE_ANALYZERS_PREFIX/semgrep:latest"
rules:
- if: $SAST_DISABLED
when: never
- if: $SAST_EXCLUDED_ANALYZERS =~ /semgrep/
when: never
- if: $CI_COMMIT_BRANCH &&
$SAST_DEFAULT_ANALYZERS =~ /semgrep/ &&
$SAST_EXPERIMENTAL_FEATURES == 'true'
exists:
- '**/*.py'
sobelow-sast:
extends: .sast-analyzer
image:

View file

@ -17,7 +17,7 @@ module Gitlab
# to a structured log
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def enqueue_stats_job(request_id)
return unless Feature.enabled?(:performance_bar_stats)
return unless gather_stats?
@client.sadd(GitlabPerformanceBarStatsWorker::STATS_KEY, request_id)
@ -43,6 +43,12 @@ module Gitlab
)
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def gather_stats?
return unless Feature.enabled?(:performance_bar_stats)
Gitlab.com? || Gitlab.staging? || !Rails.env.production?
end
end
end
end

View file

@ -24,8 +24,8 @@ module Gitlab
Gitlab::CurrentSettings.snowplow_enabled?
end
def event(category, action, label: nil, property: nil, value: nil, context: [], standard_context: nil)
context.push(standard_context.to_context) if standard_context
def event(category, action, label: nil, property: nil, value: nil, context: [], project: nil, user: nil, namespace: nil) # rubocop:disable Metrics/ParameterLists
context += [Tracking::StandardContext.new(project: project, user: user, namespace: namespace).to_context]
snowplow.event(category, action, label: label, property: property, value: value, context: context)
product_analytics.event(category, action, label: label, property: property, value: value, context: context)

View file

@ -29,11 +29,10 @@ module Gitlab
private
def to_h
public_methods(false).each_with_object({}) do |method, hash|
next if method == :to_context
hash[method] = public_send(method) # rubocop:disable GitlabSecurity/PublicSend
end.merge(@data)
{
environment: environment,
source: source
}.merge(@data)
end
end
end

View file

@ -8,7 +8,10 @@ namespace :gitlab do
task migrate_legacy_storage: :gitlab_environment do
logger.info('Starting to migrate legacy pages storage to zip deployments')
result = ::Pages::MigrateFromLegacyStorageService.new(logger, migration_threads, batch_size).execute
result = ::Pages::MigrateFromLegacyStorageService.new(logger,
migration_threads: migration_threads,
batch_size: batch_size,
ignore_invalid_entries: ignore_invalid_entries).execute
logger.info("A total of #{result[:migrated] + result[:errored]} projects were processed.")
logger.info("- The #{result[:migrated]} projects migrated successfully")
@ -42,5 +45,11 @@ namespace :gitlab do
def batch_size
ENV.fetch('PAGES_MIGRATION_BATCH_SIZE', '10').to_i
end
def ignore_invalid_entries
Gitlab::Utils.to_boolean(
ENV.fetch('PAGES_MIGRATION_IGNORE_INVALID_ENTRIES', 'false')
)
end
end
end

View file

@ -9224,6 +9224,9 @@ msgstr ""
msgid "DastProfiles|Request headers"
msgstr ""
msgid "DastProfiles|Run scan"
msgstr ""
msgid "DastProfiles|Run the AJAX spider, in addition to the traditional spider, to crawl the target site."
msgstr ""
@ -21480,9 +21483,6 @@ msgstr ""
msgid "PerformanceBar|SQL queries"
msgstr ""
msgid "PerformanceBar|Stats"
msgstr ""
msgid "PerformanceBar|trace"
msgstr ""

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module RuboCop
module Cop
# Cop that blacklists keyword arguments usage in Sidekiq workers

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Gitlab

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module RuboCop
module Cop
module Gitlab

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'set'
require_relative '../../migration_helpers'

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative '../../migration_helpers'
module RuboCop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module RuboCop
module Cop
class ProjectPathHelper < RuboCop::Cop::Cop

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module RuboCop
# Module containing helper methods for writing migration cops.
module MigrationHelpers

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
module RuboCop
# Module containing helper methods for writing QA cops.
module QAHelpers

View file

@ -7,6 +7,43 @@ RSpec.describe HelpController do
let(:user) { create(:user) }
shared_examples 'documentation pages local render' do
it 'renders HTML' do
aggregate_failures do
is_expected.to render_template('show.html.haml')
expect(response.media_type).to eq 'text/html'
end
end
end
shared_examples 'documentation pages redirect' do |documentation_base_url|
let(:gitlab_version) { '13.4.0-ee' }
before do
stub_version(gitlab_version, 'ignored_revision_value')
end
it 'redirects user to custom documentation url with a specified version' do
is_expected.to redirect_to("#{documentation_base_url}/13.4/ee/#{path}.html")
end
context 'when it is a pre-release' do
let(:gitlab_version) { '13.4.0-pre' }
it 'redirects user to custom documentation url without a version' do
is_expected.to redirect_to("#{documentation_base_url}/ee/#{path}.html")
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(help_page_documentation_redirect: false)
end
it_behaves_like 'documentation pages local render'
end
end
before do
sign_in(user)
end
@ -99,69 +136,70 @@ RSpec.describe HelpController do
describe 'GET #show' do
context 'for Markdown formats' do
subject { get :show, params: { path: path }, format: :md }
let(:path) { 'ssh/README' }
context 'when requested file exists' do
before do
expect_file_read(File.join(Rails.root, 'doc/ssh/README.md'), content: fixture_file('blockquote_fence_after.md'))
get :show, params: { path: 'ssh/README' }, format: :md
subject
end
it 'assigns to @markdown' do
expect(assigns[:markdown]).not_to be_empty
end
it 'renders HTML' do
aggregate_failures do
expect(response).to render_template('show.html.haml')
expect(response.media_type).to eq 'text/html'
end
it_behaves_like 'documentation pages local render'
end
context 'when a custom help_page_documentation_url is set in database' do
before do
stub_application_setting(help_page_documentation_base_url: 'https://in-db.gitlab.com')
end
it_behaves_like 'documentation pages redirect', 'https://in-db.gitlab.com'
end
context 'when a custom help_page_documentation_url is set in configuration file' do
let(:host) { 'https://in-yaml.gitlab.com' }
let(:docs_enabled) { true }
before do
allow(Settings).to receive(:gitlab_docs) { double(enabled: docs_enabled, host: host) }
end
it_behaves_like 'documentation pages redirect', 'https://in-yaml.gitlab.com'
context 'when gitlab_docs is disabled' do
let(:docs_enabled) { false }
it_behaves_like 'documentation pages local render'
end
context 'when host is missing' do
let(:host) { nil }
it_behaves_like 'documentation pages local render'
end
end
context 'when a custom help_page_documentation_url is set' do
context 'when help_page_documentation_url is set in both db and configuration file' do
before do
stub_application_setting(help_page_documentation_base_url: documentation_base_url)
stub_version(gitlab_version, 'deadbeaf')
stub_application_setting(help_page_documentation_base_url: 'https://in-db.gitlab.com')
allow(Settings).to receive(:gitlab_docs) { double(enabled: true, host: 'https://in-yaml.gitlab.com') }
end
subject { get :show, params: { path: path }, format: 'html' }
it_behaves_like 'documentation pages redirect', 'https://in-yaml.gitlab.com'
end
let(:gitlab_version) { '13.4.0-ee' }
let(:documentation_base_url) { 'https://docs.gitlab.com' }
let(:path) { 'ssh/README' }
it 'redirects user to custom documentation url with a specified version' do
is_expected.to redirect_to("#{documentation_base_url}/13.4/ee/#{path}.html")
context 'when help_page_documentation_url has a trailing slash' do
before do
allow(Settings).to receive(:gitlab_docs) { double(enabled: true, host: 'https://in-yaml.gitlab.com/') }
end
context 'when documentation url ends with a slash' do
let(:documentation_base_url) { 'https://docs.gitlab.com/' }
it 'redirects user to custom documentation url without slash duplicates' do
is_expected.to redirect_to("https://docs.gitlab.com/13.4/ee/#{path}.html")
end
end
context 'when it is a pre-release' do
let(:gitlab_version) { '13.4.0-pre' }
it 'redirects user to custom documentation url without a version' do
is_expected.to redirect_to("#{documentation_base_url}/ee/#{path}.html")
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(help_page_documentation_redirect: false)
end
it 'renders HTML' do
aggregate_failures do
is_expected.to render_template('show.html.haml')
expect(response.media_type).to eq 'text/html'
end
end
end
it_behaves_like 'documentation pages redirect', 'https://in-yaml.gitlab.com'
end
context 'when requested file is missing' do

View file

@ -49,10 +49,6 @@ RSpec.describe 'User can display performance bar', :js do
let(:group) { create(:group) }
before do
allow(GitlabPerformanceBarStatsWorker).to receive(:perform_in)
end
context 'when user is logged-out' do
before do
visit root_path
@ -101,28 +97,6 @@ RSpec.describe 'User can display performance bar', :js do
it_behaves_like 'performance bar is enabled by default in development'
it_behaves_like 'performance bar can be displayed'
it 'does not show Stats link by default' do
find('body').native.send_keys('pb')
wait_for_requests
expect(page).not_to have_link('Stats')
end
context 'when GITLAB_PERFORMANCE_BAR_STATS_URL environment variable is set' do
let(:stats_url) { 'https://log.gprd.gitlab.net/app/dashboards#/view/' }
before do
stub_env('GITLAB_PERFORMANCE_BAR_STATS_URL', stats_url)
end
it 'shows Stats link' do
find('body').native.send_keys('pb')
wait_for_requests
expect(page).to have_link('Stats', href: stats_url)
end
end
end
end
end

View file

@ -1,46 +1,42 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import { shallowMount } from '@vue/test-utils';
import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
describe('DelayedJobMixin', () => {
let wrapper;
const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const dummyComponent = Vue.extend({
mixins: [delayedJobMixin],
const dummyComponent = {
props: {
job: {
type: Object,
required: true,
},
},
render(createElement) {
return createElement('div', this.remainingTime);
},
});
let vm;
mixins: [delayedJobMixin],
template: '<div>{{remainingTime}}</div>',
};
afterEach(() => {
vm.$destroy();
jest.clearAllTimers();
wrapper.destroy();
wrapper = null;
});
describe('if job is empty object', () => {
beforeEach(() => {
vm = mountComponent(dummyComponent, {
job: {},
wrapper = shallowMount(dummyComponent, {
propsData: {
job: {},
},
});
});
it('sets remaining time to 00:00:00', () => {
expect(vm.$el.innerText).toBe('00:00:00');
expect(wrapper.text()).toBe('00:00:00');
});
describe('after mounting', () => {
beforeEach(() => vm.$nextTick());
it('does not update remaining time after mounting', async () => {
await wrapper.vm.$nextTick();
it('does not update remaining time', () => {
expect(vm.$el.innerText).toBe('00:00:00');
});
expect(wrapper.text()).toBe('00:00:00');
});
});
@ -48,33 +44,32 @@ describe('DelayedJobMixin', () => {
describe('if job is delayed job', () => {
let remainingTimeInMilliseconds = 42000;
beforeEach(() => {
beforeEach(async () => {
jest
.spyOn(Date, 'now')
.mockImplementation(
() => new Date(delayedJobFixture.scheduled_at).getTime() - remainingTimeInMilliseconds,
);
vm = mountComponent(dummyComponent, {
job: delayedJobFixture,
wrapper = shallowMount(dummyComponent, {
propsData: {
job: delayedJobFixture,
},
});
await wrapper.vm.$nextTick();
});
describe('after mounting', () => {
beforeEach(() => vm.$nextTick());
it('sets remaining time', () => {
expect(wrapper.text()).toBe('00:00:42');
});
it('sets remaining time', () => {
expect(vm.$el.innerText).toBe('00:00:42');
});
it('updates remaining time', async () => {
remainingTimeInMilliseconds = 41000;
jest.advanceTimersByTime(1000);
it('updates remaining time', () => {
remainingTimeInMilliseconds = 41000;
jest.advanceTimersByTime(1000);
return vm.$nextTick().then(() => {
expect(vm.$el.innerText).toBe('00:00:41');
});
});
await wrapper.vm.$nextTick();
expect(wrapper.text()).toBe('00:00:41');
});
});
});
@ -96,33 +91,32 @@ describe('DelayedJobMixin', () => {
describe('if job is delayed job', () => {
let remainingTimeInMilliseconds = 42000;
beforeEach(() => {
beforeEach(async () => {
jest
.spyOn(Date, 'now')
.mockImplementation(
() => mockGraphQlJob.scheduledAt.getTime() - remainingTimeInMilliseconds,
);
vm = mountComponent(dummyComponent, {
job: mockGraphQlJob,
wrapper = shallowMount(dummyComponent, {
propsData: {
job: mockGraphQlJob,
},
});
await wrapper.vm.$nextTick();
});
describe('after mounting', () => {
beforeEach(() => vm.$nextTick());
it('sets remaining time', () => {
expect(wrapper.text()).toBe('00:00:42');
});
it('sets remaining time', () => {
expect(vm.$el.innerText).toBe('00:00:42');
});
it('updates remaining time', async () => {
remainingTimeInMilliseconds = 41000;
jest.advanceTimersByTime(1000);
it('updates remaining time', () => {
remainingTimeInMilliseconds = 41000;
jest.advanceTimersByTime(1000);
return vm.$nextTick().then(() => {
expect(vm.$el.innerText).toBe('00:00:41');
});
});
await wrapper.vm.$nextTick();
expect(wrapper.text()).toBe('00:00:41');
});
});
});

View file

@ -9,7 +9,6 @@ describe('performance bar app', () => {
store,
env: 'development',
requestId: '123',
statsUrl: 'https://log.gprd.gitlab.net/app/dashboards#/view/',
peekUrl: '/-/peek/results',
profileUrl: '?lineprofiler=true',
},

View file

@ -19,7 +19,6 @@ describe('performance bar wrapper', () => {
peekWrapper.setAttribute('data-env', 'development');
peekWrapper.setAttribute('data-request-id', '123');
peekWrapper.setAttribute('data-peek-url', '/-/peek/results');
peekWrapper.setAttribute('data-stats-url', 'https://log.gprd.gitlab.net/app/dashboards#/view/');
peekWrapper.setAttribute('data-profile-url', '?lineprofiler=true');
mock = new MockAdapter(axios);

View file

@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import CommitComponent from '~/vue_shared/components/commit.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('Commit component', () => {
let props;
@ -13,11 +14,14 @@ describe('Commit component', () => {
};
const findUserAvatar = () => wrapper.find(UserAvatarLink);
const findRefName = () => wrapper.findByTestId('ref-name');
const createComponent = (propsData) => {
wrapper = shallowMount(CommitComponent, {
propsData,
});
wrapper = extendedWrapper(
shallowMount(CommitComponent, {
propsData,
}),
);
};
afterEach(() => {
@ -223,4 +227,20 @@ describe('Commit component', () => {
expect(wrapper.find('.ref-name').exists()).toBe(false);
});
});
describe('When commitRef has a path property instead of ref_url property', () => {
it('should render path as href attribute', () => {
props = {
commitRef: {
name: 'master',
path: 'http://localhost/namespace2/gitlabhq/tree/master',
},
};
createComponent(props);
expect(findRefName().exists()).toBe(true);
expect(findRefName().attributes('href')).toBe(props.commitRef.path);
});
});
});

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::EventType do
specify { expect(described_class.graphql_name).to eq('Event') }
specify { expect(described_class).to require_graphql_authorizations(:read_event) }
specify { expect(described_class).to have_graphql_fields(:id, :author, :action, :created_at, :updated_at) }
end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::EventableType do
it 'exposes events field' do
expect(described_class).to have_graphql_fields(:events)
end
end

View file

@ -9,38 +9,36 @@ RSpec.describe Gitlab::Tracking::StandardContext do
let(:snowplow_context) { subject.to_context }
describe '#to_context' do
context 'default fields' do
context 'environment' do
shared_examples 'contains environment' do |expected_environment|
it 'contains environment' do
expect(snowplow_context.to_json.dig(:data, :environment)).to eq(expected_environment)
end
end
context 'development or test' do
include_examples 'contains environment', 'development'
end
context 'staging' do
before do
allow(Gitlab).to receive(:staging?).and_return(true)
end
include_examples 'contains environment', 'staging'
end
context 'production' do
before do
allow(Gitlab).to receive(:com_and_canary?).and_return(true)
end
include_examples 'contains environment', 'production'
context 'environment' do
shared_examples 'contains environment' do |expected_environment|
it 'contains environment' do
expect(snowplow_context.to_json.dig(:data, :environment)).to eq(expected_environment)
end
end
it 'contains source' do
expect(snowplow_context.to_json.dig(:data, :source)).to eq(described_class::GITLAB_RAILS_SOURCE)
context 'development or test' do
include_examples 'contains environment', 'development'
end
context 'staging' do
before do
allow(Gitlab).to receive(:staging?).and_return(true)
end
include_examples 'contains environment', 'staging'
end
context 'production' do
before do
allow(Gitlab).to receive(:com_and_canary?).and_return(true)
end
include_examples 'contains environment', 'production'
end
end
it 'contains source' do
expect(snowplow_context.to_json.dig(:data, :source)).to eq(described_class::GITLAB_RAILS_SOURCE)
end
context 'with extra data' do
@ -51,31 +49,8 @@ RSpec.describe Gitlab::Tracking::StandardContext do
end
end
context 'with namespace' do
subject { described_class.new(namespace: namespace) }
it 'creates a Snowplow context without namespace and project' do
expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil
expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil
end
end
context 'with project' do
subject { described_class.new(project: project) }
it 'creates a Snowplow context without namespace and project' do
expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil
expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil
end
end
context 'with project and namespace' do
subject { described_class.new(namespace: namespace, project: project) }
it 'creates a Snowplow context without namespace and project' do
expect(snowplow_context.to_json.dig(:data, :namespace_id)).to be_nil
expect(snowplow_context.to_json.dig(:data, :project_id)).to be_nil
end
it 'does not contain any ids' do
expect(snowplow_context.to_json[:data].keys).not_to include(:user_id, :project_id, :namespace_id)
end
end
end

View file

@ -42,36 +42,31 @@ RSpec.describe Gitlab::Tracking do
end
shared_examples 'delegates to destination' do |klass|
context 'with standard context' do
it "delegates to #{klass} destination" do
expect_any_instance_of(klass).to receive(:event) do |_, category, action, args|
expect(category).to eq('category')
expect(action).to eq('action')
expect(args[:label]).to eq('label')
expect(args[:property]).to eq('property')
expect(args[:value]).to eq(1.5)
expect(args[:context].length).to eq(1)
expect(args[:context].first.to_json[:schema]).to eq(Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL)
expect(args[:context].first.to_json[:data]).to include(foo: 'bar')
end
it "delegates to #{klass} destination" do
other_context = double(:context)
described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5,
standard_context: Gitlab::Tracking::StandardContext.new(foo: 'bar'))
project = double(:project)
user = double(:user)
namespace = double(:namespace)
expect(Gitlab::Tracking::StandardContext)
.to receive(:new)
.with(project: project, user: user, namespace: namespace)
.and_call_original
expect_any_instance_of(klass).to receive(:event) do |_, category, action, args|
expect(category).to eq('category')
expect(action).to eq('action')
expect(args[:label]).to eq('label')
expect(args[:property]).to eq('property')
expect(args[:value]).to eq(1.5)
expect(args[:context].length).to eq(2)
expect(args[:context].first).to eq(other_context)
expect(args[:context].last.to_json[:schema]).to eq(Gitlab::Tracking::StandardContext::GITLAB_STANDARD_SCHEMA_URL)
end
end
context 'without standard context' do
it "delegates to #{klass} destination" do
expect_any_instance_of(klass).to receive(:event) do |_, category, action, args|
expect(category).to eq('category')
expect(action).to eq('action')
expect(args[:label]).to eq('label')
expect(args[:property]).to eq('property')
expect(args[:value]).to eq(1.5)
end
described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5)
end
described_class.event('category', 'action', label: 'label', property: 'property', value: 1.5,
context: [other_context], project: project, user: user, namespace: namespace)
end
end

View file

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Pages::MigrateFromLegacyStorageService do
let(:service) { described_class.new(Rails.logger, 3, 10) }
let(:service) { described_class.new(Rails.logger, migration_threads: 3, batch_size: 10, ignore_invalid_entries: false) }
it 'does not try to migrate pages if pages are not deployed' do
expect(::Pages::MigrateLegacyStorageToDeploymentService).not_to receive(:new)
@ -22,7 +22,7 @@ RSpec.describe Pages::MigrateFromLegacyStorageService do
end
end
service = described_class.new(Rails.logger, 3, 2)
service = described_class.new(Rails.logger, migration_threads: 3, batch_size: 2, ignore_invalid_entries: false)
threads = Concurrent::Set.new
@ -49,7 +49,7 @@ RSpec.describe Pages::MigrateFromLegacyStorageService do
context 'when pages directory does not exist' do
it 'tries to migrate the project, but does not crash' do
expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project) do |service|
expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false) do |service|
expect(service).to receive(:execute).and_call_original
end
@ -66,7 +66,7 @@ RSpec.describe Pages::MigrateFromLegacyStorageService do
end
it 'migrates pages projects without deployments' do
expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project) do |service|
expect_next_instance_of(::Pages::MigrateLegacyStorageToDeploymentService, project, ignore_invalid_entries: false) do |service|
expect(service).to receive(:execute).and_call_original
end

View file

@ -6,6 +6,14 @@ RSpec.describe Pages::MigrateLegacyStorageToDeploymentService do
let(:project) { create(:project, :repository) }
let(:service) { described_class.new(project) }
it 'calls ::Pages::ZipDirectoryService' do
expect_next_instance_of(::Pages::ZipDirectoryService, project.pages_path, ignore_invalid_entries: true) do |zip_service|
expect(zip_service).to receive(:execute).and_call_original
end
expect(described_class.new(project, ignore_invalid_entries: true).execute[:status]).to eq(:error)
end
it 'marks pages as not deployed if public directory is absent' do
project.mark_pages_as_deployed

View file

@ -10,8 +10,14 @@ RSpec.describe Pages::ZipDirectoryService do
end
end
let(:ignore_invalid_entries) { false }
let(:service) do
described_class.new(@work_dir, ignore_invalid_entries: ignore_invalid_entries)
end
let(:result) do
described_class.new(@work_dir).execute
service.execute
end
let(:status) { result[:status] }
@ -20,6 +26,8 @@ RSpec.describe Pages::ZipDirectoryService do
let(:entries_count) { result[:entries_count] }
it 'returns error if project pages dir does not exist' do
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
expect(
described_class.new("/tmp/not/existing/dir").execute
).to eq(status: :error, message: "Can not find valid public dir in /tmp/not/existing/dir")
@ -132,32 +140,69 @@ RSpec.describe Pages::ZipDirectoryService do
end
end
it 'ignores the symlink pointing outside of public directory' do
create_file("target.html", "hello")
create_link("public/link.html", "../target.html")
shared_examples "raises or ignores file" do |raised_exception, file|
it 'raises error' do
expect do
result
end.to raise_error(raised_exception)
end
with_zip_file do |zip_file|
expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT)
context 'when errors are ignored' do
let(:ignore_invalid_entries) { true }
it 'does not create entry' do
with_zip_file do |zip_file|
expect { zip_file.get_entry(file) }.to raise_error(Errno::ENOENT)
end
end
end
end
it 'ignores the symlink if target is absent' do
create_link("public/link.html", "./target.html")
with_zip_file do |zip_file|
expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT)
context 'when symlink points outside of public directory' do
before do
create_file("target.html", "hello")
create_link("public/link.html", "../target.html")
end
include_examples "raises or ignores file", described_class::InvalidEntryError, "public/link.html"
end
it 'ignores symlink if is absolute and points to outside of directory' do
target = File.join(@work_dir, "target")
FileUtils.touch(target)
create_link("public/link.html", target)
with_zip_file do |zip_file|
expect { zip_file.get_entry("public/link.html") }.to raise_error(Errno::ENOENT)
context 'when target of the symlink is absent' do
before do
create_link("public/link.html", "./target.html")
end
include_examples "raises or ignores file", Errno::ENOENT, "public/link.html"
end
context 'when targets itself' do
before do
create_link("public/link.html", "./link.html")
end
include_examples "raises or ignores file", Errno::ELOOP, "public/link.html"
end
context 'when symlink is absolute and points to outside of directory' do
before do
target = File.join(@work_dir, "target")
FileUtils.touch(target)
create_link("public/link.html", target)
end
include_examples "raises or ignores file", described_class::InvalidEntryError, "public/link.html"
end
context 'when entry has unknown ftype' do
before do
file = create_file("public/index.html", "hello")
allow(File).to receive(:lstat).and_call_original
expect(File).to receive(:lstat).with(file) { double("lstat", ftype: "unknown") }
end
include_examples "raises or ignores file", described_class::InvalidEntryError, "public/index.html"
end
it "includes raw symlink if it's target is a valid directory" do
@ -204,9 +249,13 @@ RSpec.describe Pages::ZipDirectoryService do
end
def create_file(name, content)
File.open(File.join(@work_dir, name), "w") do |f|
file_path = File.join(@work_dir, name)
File.open(file_path, "w") do |f|
f.write(content)
end
file_path
end
def create_dir(dir)

View file

@ -46,7 +46,7 @@ module SnowplowHelpers
# }
# ]
# )
def expect_snowplow_event(category:, action:, context: nil, standard_context: nil, **kwargs)
def expect_snowplow_event(category:, action:, context: nil, **kwargs)
if context
kwargs[:context] = []
context.each do |c|
@ -56,14 +56,6 @@ module SnowplowHelpers
end
end
if standard_context
expect(Gitlab::Tracking::StandardContext)
.to have_received(:new)
.with(**standard_context)
kwargs[:standard_context] = an_instance_of(Gitlab::Tracking::StandardContext)
end
expect(Gitlab::Tracking).to have_received(:event) # rubocop:disable RSpec/ExpectGitlabTracking
.with(category, action, **kwargs).at_least(:once)
end

View file

@ -18,7 +18,6 @@ RSpec.configure do |config|
stub_application_setting(snowplow_enabled: true)
allow(SnowplowTracker::SelfDescribingJson).to receive(:new).and_call_original
allow(Gitlab::Tracking::StandardContext).to receive(:new).and_call_original
allow(Gitlab::Tracking).to receive(:event).and_call_original # rubocop:disable RSpec/ExpectGitlabTracking
end

View file

@ -11,7 +11,10 @@ RSpec.describe 'gitlab:pages' do
subject { run_rake_task('gitlab:pages:migrate_legacy_storage') }
it 'calls migration service' do
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, 3, 10) do |service|
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
migration_threads: 3,
batch_size: 10,
ignore_invalid_entries: false) do |service|
expect(service).to receive(:execute).and_call_original
end
@ -21,7 +24,10 @@ RSpec.describe 'gitlab:pages' do
it 'uses PAGES_MIGRATION_THREADS environment variable' do
stub_env('PAGES_MIGRATION_THREADS', '5')
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, 5, 10) do |service|
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
migration_threads: 5,
batch_size: 10,
ignore_invalid_entries: false) do |service|
expect(service).to receive(:execute).and_call_original
end
@ -31,7 +37,23 @@ RSpec.describe 'gitlab:pages' do
it 'uses PAGES_MIGRATION_BATCH_SIZE environment variable' do
stub_env('PAGES_MIGRATION_BATCH_SIZE', '100')
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything, 3, 100) do |service|
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
migration_threads: 3,
batch_size: 100,
ignore_invalid_entries: false) do |service|
expect(service).to receive(:execute).and_call_original
end
subject
end
it 'uses PAGES_MIGRATION_IGNORE_INVALID_ENTRIES environment variable' do
stub_env('PAGES_MIGRATION_IGNORE_INVALID_ENTRIES', 'true')
expect_next_instance_of(::Pages::MigrateFromLegacyStorageService, anything,
migration_threads: 3,
batch_size: 10,
ignore_invalid_entries: true) do |service|
expect(service).to receive(:execute).and_call_original
end