Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-16 12:10:20 +00:00
parent ead25aaac3
commit ea3aadb056
82 changed files with 2370 additions and 870 deletions

View File

@ -16,6 +16,7 @@
- export GOPATH=$CI_PROJECT_DIR/.go
- mkdir -p $GOPATH
- source scripts/utils.sh
- log_disk_usage # https://gitlab.com/gitlab-org/gitlab/-/issues/478880
.default-before_script:
before_script:

View File

@ -377,16 +377,26 @@ rspec-ee unit clickhouse:
- .rspec-base-pg14-clickhouse23
- .rails:rules:ee-only-clickhouse-changes
rspec ci-config-validation:
rspec ci-config-validation branch-pipelines:
extends:
- .rspec-base-pg14
- .rails:rules:rspec-ci-config-validation
stage: test
- .ci-config-validation-base
script:
- !reference [.base-script, script]
- rspec_section rspec_simple_job "--tag ci_config_validation -- spec/dot_gitlab_ci/"
after_script:
- echo 'OK' # override after_script from .rspec-base
- rspec_section rspec_simple_job "--tag ci_config_validation -- spec/dot_gitlab_ci/ci_configuration_validation/branch_pipeline_spec.rb"
rspec ci-config-validation pipeline-labels:
extends:
- .ci-config-validation-base
script:
- !reference [.base-script, script]
- rspec_section rspec_simple_job "--tag ci_config_validation -- spec/dot_gitlab_ci/ci_configuration_validation/pipeline_labels_spec.rb"
rspec ci-config-validation mr-pipelines:
extends:
- .ci-config-validation-base
script:
- !reference [.base-script, script]
- rspec_section rspec_simple_job "--tag ci_config_validation -- spec/dot_gitlab_ci/ci_configuration_validation/merge_request_pipeline_spec.rb"
gitlab:clickhouse:rollback:main:
extends:

View File

@ -297,6 +297,13 @@ include:
- .use-pg16-opensearch2-ee
- .rails:rules:run-search-tests
.ci-config-validation-base:
extends:
- .rspec-base-pg14
- .rails:rules:rspec-ci-config-validation
stage: test
after_script: [] # override after_script from .rspec-base
.db-job-base:
extends:
- .rails-job-base

View File

@ -1 +1 @@
83d81744aa7215577e8b0bef40723a787de26793
ab05fbe1d1f84e10fa4a28d857b6a08429ebe90b

View File

@ -243,7 +243,11 @@ export default {
return;
}
e.preventDefault();
this.$emit('select-issuable', { iid: this.issuableIid, webUrl: this.issuableLinkHref });
this.$emit('select-issuable', {
iid: this.issuableIid,
webUrl: this.issuableLinkHref,
fullPath: this.workItemFullPath,
});
},
},
};

View File

@ -84,7 +84,7 @@ export default {
},
isUpdating: {
handler(isUpdating) {
this.sortable.option('disabled', isUpdating);
this.sortable?.option('disabled', isUpdating);
},
},
},

View File

@ -150,7 +150,7 @@ export default {
if (isEmpty(this.workItem)) {
this.setEmptyState();
}
if (!this.isModal && this.workItem.namespace) {
if (!(this.isModal || this.isDrawer) && this.workItem.namespace) {
const path = this.workItem.namespace.fullPath
? ` · ${this.workItem.namespace.fullPath}`
: '';

View File

@ -1,7 +1,9 @@
<script>
import { GlLink, GlDrawer } from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { visitUrl } from '~/lib/utils/url_utility';
export default {
name: 'WorkItemDrawer',
@ -10,6 +12,7 @@ export default {
GlDrawer,
WorkItemDetail: () => import('~/work_items/components/work_item_detail.vue'),
},
inject: ['fullPath'],
inheritAttrs: false,
props: {
open: {
@ -22,6 +25,11 @@ export default {
default: () => ({}),
},
},
computed: {
activeItemFullPath() {
return this.activeItem?.fullPath;
},
},
methods: {
async deleteWorkItem({ workItemId }) {
try {
@ -38,6 +46,24 @@ export default {
Sentry.captureException(error);
}
},
redirectToWorkItem() {
const workItem = this.activeItem;
const escapedFullPath = escapeRegExp(this.fullPath);
// eslint-disable-next-line no-useless-escape
const regex = new RegExp(`groups\/${escapedFullPath}\/-\/(work_items|epics)\/\\d+`);
const isWorkItemPath = regex.test(workItem.webUrl);
if (isWorkItemPath) {
this.$router.push({
name: 'workItem',
params: {
iid: workItem.iid,
},
});
} else {
visitUrl(workItem.webUrl);
}
},
},
};
</script>
@ -51,14 +77,18 @@ export default {
@close="$emit('close')"
>
<template #title>
<gl-link :href="activeItem.webUrl" class="gl-text-black-normal">{{
__('Open full view')
}}</gl-link>
<gl-link
class="gl-text-black-normal"
:href="activeItem.webUrl"
@click.prevent="redirectToWorkItem"
>{{ __('Open full view') }}</gl-link
>
</template>
<template #default>
<work-item-detail
:key="activeItem.iid"
:work-item-iid="activeItem.iid"
:modal-work-item-full-path="activeItemFullPath"
is-drawer
class="gl-pt-0! work-item-drawer"
@deleteWorkItem="deleteWorkItem"

View File

@ -1,5 +1,6 @@
<script>
import { GlFilteredSearchToken, GlLoadingIcon } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
@ -13,7 +14,6 @@ import {
} from 'ee_else_ce/issues/list/utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { visitUrl } from '~/lib/utils/url_utility';
import {
STATUS_ALL,
STATUS_CLOSED,
@ -50,9 +50,10 @@ import {
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { STATE_CLOSED, WORK_ITEM_TYPE_ENUM_EPIC } from '../constants';
import { STATE_CLOSED, STATE_OPEN, WORK_ITEM_TYPE_ENUM_EPIC } from '../constants';
import getWorkItemsQuery from '../graphql/list/get_work_items.query.graphql';
import { sortOptions, urlSortParams } from './list/constants';
@ -66,6 +67,11 @@ const MilestoneToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
const statusMap = {
[STATUS_OPEN]: STATE_OPEN,
[STATUS_CLOSED]: STATE_CLOSED,
};
export default {
issuableListTabs,
sortOptions,
@ -74,6 +80,7 @@ export default {
IssuableList,
IssueCardStatistics,
IssueCardTimeInfo,
WorkItemDrawer,
},
mixins: [glFeatureFlagMixin()],
inject: [
@ -112,6 +119,8 @@ export default {
state: STATUS_OPEN,
tabCounts: {},
workItems: [],
activeItem: null,
isRefetching: false,
};
},
apollo: {
@ -163,6 +172,9 @@ export default {
},
},
computed: {
isItemSelected() {
return !isEmpty(this.activeItem);
},
apiFilterParams() {
return convertToApiParams(this.filterTokens);
},
@ -173,11 +185,14 @@ export default {
hasQualityManagementFeature: this.hasQualityManagementFeature,
});
},
workItemDrawerEnabled() {
return this.glFeatures?.issuesListDrawer;
},
hasSearch() {
return Boolean(this.searchQuery);
},
isLoading() {
return this.$apollo.queries.workItems.loading;
return this.$apollo.queries.workItems.loading && !this.isRefetching;
},
isOpenTab() {
return this.state === STATUS_OPEN;
@ -339,6 +354,9 @@ export default {
this.autocompleteCache = new AutocompleteCache();
},
methods: {
handleSelect(item) {
this.activeItem = item;
},
fetchEmojis(search) {
return this.autocompleteCache.fetch({
url: this.autocompleteAwardEmojisPath,
@ -428,20 +446,22 @@ export default {
Sentry.captureException(error);
});
},
redirectToWorkItem(workItem) {
const regex = /groups\/.+?\/-\/work_items\/\d+/;
const isWorkItemPath = regex.test(workItem.webUrl);
if (isWorkItemPath) {
this.$router.push({
name: 'workItem',
params: {
iid: workItem.iid,
},
});
} else {
visitUrl(workItem.webUrl);
deleteItem() {
this.activeItem = null;
this.refetchItems();
},
handleStatusChange(workItem) {
if (this.state === STATUS_ALL) {
return;
}
if (statusMap[this.state] !== workItem.state) {
this.refetchItems();
}
},
async refetchItems() {
this.isRefetching = true;
await this.$apollo.queries.workItems.refetch();
this.isRefetching = false;
},
},
};
@ -450,70 +470,80 @@ export default {
<template>
<gl-loading-icon v-if="!isInitialAllCountSet && !error" class="gl-mt-5" size="lg" />
<issuable-list
v-else-if="hasAnyIssues || error"
:current-tab="state"
:default-page-size="pageSize"
:error="error"
:has-next-page="pageInfo.hasNextPage"
:has-previous-page="pageInfo.hasPreviousPage"
:initial-sort-by="sortKey"
:issuables="workItems"
:issuables-loading="isLoading"
:show-bulk-edit-sidebar="showBulkEditSidebar"
namespace="work-items"
recent-searches-storage-key="issues"
:search-tokens="searchTokens"
show-filtered-search-friendly-text
:show-page-size-selector="showPageSizeSelector"
:show-pagination-controls="showPaginationControls"
show-work-item-type-icon
:sort-options="$options.sortOptions"
:tab-counts="tabCounts"
:tabs="$options.issuableListTabs"
use-keyset-pagination
prevent-redirect
@click-tab="handleClickTab"
@dismiss-alert="error = undefined"
@filter="handleFilter"
@next-page="handleNextPage"
@page-size-change="handlePageSizeChange"
@previous-page="handlePreviousPage"
@sort="handleSort"
@select-issuable="redirectToWorkItem"
>
<template #nav-actions>
<slot name="nav-actions"></slot>
</template>
<div v-else-if="hasAnyIssues || error">
<work-item-drawer
v-if="workItemDrawerEnabled"
:active-item="activeItem"
:open="isItemSelected"
@close="activeItem = null"
@addChild="refetchItems"
@workItemDeleted="deleteItem"
@work-item-updated="handleStatusChange"
/>
<issuable-list
:current-tab="state"
:default-page-size="pageSize"
:error="error"
:has-next-page="pageInfo.hasNextPage"
:has-previous-page="pageInfo.hasPreviousPage"
:initial-sort-by="sortKey"
:issuables="workItems"
:issuables-loading="isLoading"
:show-bulk-edit-sidebar="showBulkEditSidebar"
namespace="work-items"
recent-searches-storage-key="issues"
:search-tokens="searchTokens"
show-filtered-search-friendly-text
:show-page-size-selector="showPageSizeSelector"
:show-pagination-controls="showPaginationControls"
show-work-item-type-icon
:sort-options="$options.sortOptions"
:tab-counts="tabCounts"
:tabs="$options.issuableListTabs"
use-keyset-pagination
:prevent-redirect="workItemDrawerEnabled"
@click-tab="handleClickTab"
@dismiss-alert="error = undefined"
@filter="handleFilter"
@next-page="handleNextPage"
@page-size-change="handlePageSizeChange"
@previous-page="handlePreviousPage"
@sort="handleSort"
@select-issuable="handleSelect"
>
<template #nav-actions>
<slot name="nav-actions"></slot>
</template>
<template #timeframe="{ issuable = {} }">
<issue-card-time-info :issue="issuable" />
</template>
<template #timeframe="{ issuable = {} }">
<issue-card-time-info :issue="issuable" />
</template>
<template #status="{ issuable }">
{{ getStatus(issuable) }}
</template>
<template #status="{ issuable }">
{{ getStatus(issuable) }}
</template>
<template #statistics="{ issuable = {} }">
<issue-card-statistics :issue="issuable" />
</template>
<template #statistics="{ issuable = {} }">
<issue-card-statistics :issue="issuable" />
</template>
<template #empty-state>
<slot name="list-empty-state" :has-search="hasSearch" :is-open-tab="isOpenTab"></slot>
</template>
<template #empty-state>
<slot name="list-empty-state" :has-search="hasSearch" :is-open-tab="isOpenTab"></slot>
</template>
<template #list-body>
<slot name="list-body"></slot>
</template>
<template #list-body>
<slot name="list-body"></slot>
</template>
<template #bulk-edit-actions="{ checkedIssuables }">
<slot name="bulk-edit-actions" :checked-issuables="checkedIssuables"></slot>
</template>
<template #bulk-edit-actions="{ checkedIssuables }">
<slot name="bulk-edit-actions" :checked-issuables="checkedIssuables"></slot>
</template>
<template #sidebar-items="{ checkedIssuables }">
<slot name="sidebar-items" :checked-issuables="checkedIssuables"></slot>
</template>
</issuable-list>
<template #sidebar-items="{ checkedIssuables }">
<slot name="sidebar-items" :checked-issuables="checkedIssuables"></slot>
</template>
</issuable-list>
</div>
<div v-else>
<slot name="page-empty-state"></slot>

View File

@ -4,6 +4,8 @@ require 'gon'
require 'fogbugz'
class ApplicationController < BaseActionController
use Gitlab::Middleware::ActionControllerStaticContext
include Gitlab::GonHelper
include Gitlab::NoCacheHeaders
include GitlabRoutingHelper
@ -439,14 +441,27 @@ class ApplicationController < BaseActionController
end
def set_current_context(&block)
static_context =
if Feature.enabled?(:controller_static_context, Feature.current_request)
{} # middleware should've included caller_id and feature_category
else
{ caller_id: self.class.endpoint_id_for_action(action_name) }
end
# even though feature_category is pre-populated by
# Gitlab::Middleware::ActionControllerStaticContext
# using the static annotation on controllers, the
# controllers can override feature_category conditionally
static_context[:feature_category] = feature_category if feature_category.present?
Gitlab::ApplicationContext.push(
user: -> { context_user },
project: -> { @project if @project&.persisted? },
namespace: -> { @group if @group&.persisted? },
caller_id: self.class.endpoint_id_for_action(action_name),
remote_ip: request.ip,
feature_category: feature_category,
**http_router_rule_context
static_context.merge({
user: -> { context_user },
project: -> { @project if @project&.persisted? },
namespace: -> { @group if @group&.persisted? },
remote_ip: request.ip,
**http_router_rule_context
})
)
yield
ensure

View File

@ -148,6 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController
create_params = issue_params.merge(
add_related_issue: add_related_issue,
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
observability_links: params[:observability_links],
discussion_to_resolve: params[:discussion_to_resolve]
)

View File

@ -43,7 +43,15 @@ class Projects::JobsController < Projects::ApplicationController
render json: Ci::JobSerializer
.new(project: @project, current_user: @current_user)
.represent(@build.present(current_user: current_user), {}, BuildDetailsEntity)
.represent(
@build.present(current_user: current_user),
{
# Pipeline will show all failed builds by default if not using disable_failed_builds
disable_coverage: true,
disable_failed_builds: true
},
BuildDetailsEntity
)
end
end
end

View File

@ -9,6 +9,8 @@ module Types
connection_type_class Types::LimitedCountableConnectionType
markdown_field :description_html, null: true
field :id, ::Types::GlobalIDType[::Ml::ModelVersion], null: false, description: 'ID of the model version.'
field :created_at, Types::TimeType, null: false, description: 'Date of creation.'
@ -29,6 +31,9 @@ module Types
field :_links, ::Types::Ml::ModelVersionLinksType, null: false, method: :itself,
description: 'Map of links to perform actions on the model version.'
def description_html_resolver
::MarkupHelper.markdown(object.description, context.to_h.dup)
end
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -12,7 +12,7 @@ module Projects
can_write_model_registry: can_write_model_registry?(user, project),
mlflow_tracking_url: mlflow_tracking_url(project),
max_allowed_file_size: max_allowed_file_size(project),
markdown_preview_path: ::Gitlab::Routing.url_helpers.project_ml_preview_markdown_url(project)
markdown_preview_path: preview_markdown_path(project)
}
to_json(data)
@ -30,7 +30,7 @@ module Projects
model_name: model.name,
max_allowed_file_size: max_allowed_file_size(project),
latest_version: model.latest_version&.version,
markdown_preview_path: ::Gitlab::Routing.url_helpers.project_ml_preview_markdown_url(project)
markdown_preview_path: preview_markdown_path(project)
}
to_json(data)

View File

@ -39,6 +39,7 @@ module WorkItemsHelper
is_signed_in: current_user.present?.to_s,
show_new_issue_link: can?(current_user, :create_work_item, group).to_s,
issues_list_path: issues_group_path(group),
report_abuse_path: add_category_abuse_reports_path,
labels_manage_path: group_labels_path(group),
can_admin_label: can?(current_user, :admin_label, group).to_s
}

View File

@ -12,6 +12,8 @@ module VirtualRegistries
validates :group, top_level_group: true, presence: true, uniqueness: true
validates :cache_validity_hours, numericality: { greater_than_or_equal_to: 0, only_integer: true }
scope :for_group, ->(group) { where(group: group) }
end
end
end

View File

@ -675,7 +675,7 @@ class IssuableBaseService < ::BaseContainerService
end
def allowed_create_params(params)
params
params.except(:observability_links)
end
def allowed_update_params(params)

View File

@ -47,6 +47,8 @@
will stay unresolved. Ask someone with permission to resolve
= @discussion_to_resolve ? 'it.' : 'them.'
= render_if_exists 'observability/observability_links', observability_values: @observability_values, issuable: issuable, project: project
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
.gl-mt-5{ class: (is_footer ? "footer-block" : "middle-block") }
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = issuable.project.present.contribution_guide_path)

View File

@ -22,12 +22,7 @@
= html_escape(s_('%{time_ago}')) % { time_ago: time_ago_with_tooltip(key.created_at).html_safe}
%td{ data: { label: s_('Profiles|Last used'), testid: 'last-used' } }
-# TODO: Remove this conditional when https://gitlab.com/gitlab-org/gitlab/-/issues/324764 is resolved.
- if Feature.enabled?(:disable_ssh_key_used_tracking)
= _('Unavailable')
= link_to sprite_icon('question-o'), help_page_path('user/ssh', anchor: 'view-your-accounts-ssh-keys')
- else
= key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : _('Never')
= key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : _('Never')
%td{ data: { label: s_('Profiles|Expires'), testid: 'expires' } }
- if key.expired?

View File

@ -0,0 +1,9 @@
---
name: controller_static_context
feature_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/3593
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157628
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/470047
milestone: '17.4'
group: group::scalability
type: beta
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: disable_ssh_key_used_tracking
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54335
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/889
milestone: '13.9'
type: development
group: group::source code
default_enabled: false

View File

@ -461,6 +461,10 @@ vulnerability_occurrences:
- table: ci_pipelines
column: latest_pipeline_id
on_delete: async_nullify
vulnerability_scanners:
- table: projects
column: project_id
on_delete: async_delete
vulnerability_state_transitions:
- table: ci_pipelines
column: state_changed_at_pipeline_id

View File

@ -0,0 +1,12 @@
---
table_name: observability_logs_issues_connections
classes:
- Observability::LogsIssuesConnection
feature_categories:
- logging
description: Represents the join between an Issue and an Observability log stored in Gitlab Observability Backend.
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/162194
milestone: '17.4'
gitlab_schema: gitlab_main_cell
sharding_key:
project_id: projects

View File

@ -7,8 +7,6 @@ feature_categories:
description: Stores information about the vulnerability scanners used by projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6896
milestone: '11.4'
gitlab_schema: gitlab_main_cell
allow_cross_foreign_keys:
- gitlab_main_clusterwide
gitlab_schema: gitlab_sec
sharding_key:
project_id: projects

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class CreateLogsIssuesConnections < Gitlab::Database::Migration[2.2]
milestone '17.4'
def up
create_table :observability_logs_issues_connections do |t|
t.references :issue, null: false, foreign_key: { to_table: :issues, on_delete: :cascade }, index: false
t.bigint :project_id, null: false, index: true
t.datetime_with_timezone :log_timestamp, null: false
t.timestamps_with_timezone null: false
t.integer :severity_number, null: false, limit: 2
t.text :service_name, null: false, limit: 500
t.text :trace_identifier, null: false, limit: 128
t.text :log_fingerprint, null: false, limit: 128
t.index [:issue_id, :service_name, :severity_number, :log_timestamp, :log_fingerprint, :trace_identifier],
unique: true,
name: 'idx_o11y_log_issue_conn_on_issue_id_logs_search_metadata'
end
end
def down
drop_table :observability_logs_issues_connections
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddProjectsO11yLogsFk < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '17.4'
def up
add_concurrent_foreign_key :observability_logs_issues_connections,
:projects,
column: :project_id,
on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :observability_logs_issues_connections, column: :project_id
end
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class RemoveProjectsVulnerabilityScannersProjectIdFk < Gitlab::Database::Migration[2.2]
milestone '17.4'
disable_ddl_transaction!
FOREIGN_KEY_NAME = "fk_rails_5c9d42a221"
def up
with_lock_retries do
remove_foreign_key_if_exists(:vulnerability_scanners, :projects,
name: FOREIGN_KEY_NAME, reverse_lock_order: true)
end
end
def down
add_concurrent_foreign_key(:vulnerability_scanners, :projects,
name: FOREIGN_KEY_NAME, column: :project_id,
target_column: :id, on_delete: :cascade)
end
end

View File

@ -0,0 +1 @@
af0d599ff84d3041a9fdf145df8e302c47b27b003f47d5d5de60482dfcd169e8

View File

@ -0,0 +1 @@
df6aa8fe56933d3aa7a582e0cc8478538362b8ce0649acf4088b5d2791b00ce9

View File

@ -0,0 +1 @@
706048cbc961983c9cf50d3dfd97d30637b4f17f334bf05207d5974962a6b2dd

View File

@ -13964,6 +13964,31 @@ CREATE SEQUENCE oauth_openid_requests_id_seq
ALTER SEQUENCE oauth_openid_requests_id_seq OWNED BY oauth_openid_requests.id;
CREATE TABLE observability_logs_issues_connections (
id bigint NOT NULL,
issue_id bigint NOT NULL,
project_id bigint NOT NULL,
log_timestamp timestamp with time zone NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
severity_number smallint NOT NULL,
service_name text NOT NULL,
trace_identifier text NOT NULL,
log_fingerprint text NOT NULL,
CONSTRAINT check_80945187b6 CHECK ((char_length(trace_identifier) <= 128)),
CONSTRAINT check_ab38275544 CHECK ((char_length(log_fingerprint) <= 128)),
CONSTRAINT check_d86a20d56b CHECK ((char_length(service_name) <= 500))
);
CREATE SEQUENCE observability_logs_issues_connections_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE observability_logs_issues_connections_id_seq OWNED BY observability_logs_issues_connections.id;
CREATE TABLE observability_metrics_issues_connections (
id bigint NOT NULL,
issue_id bigint NOT NULL,
@ -21616,6 +21641,8 @@ ALTER TABLE ONLY oauth_device_grants ALTER COLUMN id SET DEFAULT nextval('oauth_
ALTER TABLE ONLY oauth_openid_requests ALTER COLUMN id SET DEFAULT nextval('oauth_openid_requests_id_seq'::regclass);
ALTER TABLE ONLY observability_logs_issues_connections ALTER COLUMN id SET DEFAULT nextval('observability_logs_issues_connections_id_seq'::regclass);
ALTER TABLE ONLY observability_metrics_issues_connections ALTER COLUMN id SET DEFAULT nextval('observability_metrics_issues_connections_id_seq'::regclass);
ALTER TABLE ONLY onboarding_progresses ALTER COLUMN id SET DEFAULT nextval('onboarding_progresses_id_seq'::regclass);
@ -23991,6 +24018,9 @@ ALTER TABLE ONLY oauth_device_grants
ALTER TABLE ONLY oauth_openid_requests
ADD CONSTRAINT oauth_openid_requests_pkey PRIMARY KEY (id);
ALTER TABLE ONLY observability_logs_issues_connections
ADD CONSTRAINT observability_logs_issues_connections_pkey PRIMARY KEY (id);
ALTER TABLE ONLY observability_metrics_issues_connections
ADD CONSTRAINT observability_metrics_issues_connections_pkey PRIMARY KEY (id);
@ -26304,6 +26334,8 @@ CREATE UNIQUE INDEX idx_namespace_settings_on_default_compliance_framework_id ON
CREATE INDEX idx_namespace_settings_on_last_dormant_member_review_at ON namespace_settings USING btree (last_dormant_member_review_at) WHERE (remove_dormant_members IS TRUE);
CREATE UNIQUE INDEX idx_o11y_log_issue_conn_on_issue_id_logs_search_metadata ON observability_logs_issues_connections USING btree (issue_id, service_name, severity_number, log_timestamp, log_fingerprint, trace_identifier);
CREATE UNIQUE INDEX idx_o11y_metric_issue_conn_on_issue_id_metric_type_name ON observability_metrics_issues_connections USING btree (issue_id, metric_type, metric_name);
CREATE UNIQUE INDEX idx_on_approval_group_rules_any_approver_type ON approval_group_rules USING btree (group_id, rule_type) WHERE (rule_type = 4);
@ -28758,6 +28790,8 @@ CREATE UNIQUE INDEX index_oauth_device_grants_on_user_code ON oauth_device_grant
CREATE INDEX index_oauth_openid_requests_on_access_grant_id ON oauth_openid_requests USING btree (access_grant_id);
CREATE INDEX index_observability_logs_issues_connections_on_project_id ON observability_logs_issues_connections USING btree (project_id);
CREATE INDEX index_observability_metrics_issues_connections_on_namespace_id ON observability_metrics_issues_connections USING btree (namespace_id);
CREATE UNIQUE INDEX index_on_deploy_keys_id_and_type_and_public ON keys USING btree (id, type) WHERE (public = true);
@ -33269,6 +33303,9 @@ ALTER TABLE ONLY catalog_resource_components
ALTER TABLE ONLY ci_build_pending_states
ADD CONSTRAINT fk_861cd17da3_p FOREIGN KEY (partition_id, build_id) REFERENCES p_ci_builds(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE;
ALTER TABLE ONLY observability_logs_issues_connections
ADD CONSTRAINT fk_86c5fb94cc FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY packages_package_files
ADD CONSTRAINT fk_86f0f182f8 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
@ -34748,9 +34785,6 @@ ALTER TABLE ONLY cluster_providers_gcp
ALTER TABLE ONLY insights
ADD CONSTRAINT fk_rails_5c4391f60a FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_scanners
ADD CONSTRAINT fk_rails_5c9d42a221 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY reviews
ADD CONSTRAINT fk_rails_5ca11d8c31 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
@ -34766,6 +34800,9 @@ ALTER TABLE ONLY packages_nuget_symbols
ALTER TABLE ONLY resource_weight_events
ADD CONSTRAINT fk_rails_5eb5cb92a1 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
ALTER TABLE ONLY observability_logs_issues_connections
ADD CONSTRAINT fk_rails_5f0f58ca3a FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
ALTER TABLE ONLY approval_project_rules
ADD CONSTRAINT fk_rails_5fb4dd100b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -27133,6 +27133,7 @@ Version of a machine learning model.
| <a id="mlmodelversioncandidate"></a>`candidate` | [`MlCandidate!`](#mlcandidate) | Metrics, params and metadata for the model version. |
| <a id="mlmodelversioncreatedat"></a>`createdAt` | [`Time!`](#time) | Date of creation. |
| <a id="mlmodelversiondescription"></a>`description` | [`String`](#string) | Description of the version. |
| <a id="mlmodelversiondescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
| <a id="mlmodelversionid"></a>`id` | [`MlModelVersionID!`](#mlmodelversionid) | ID of the model version. |
| <a id="mlmodelversionpackageid"></a>`packageId` | [`PackagesPackageID!`](#packagespackageid) | Package for model version artifacts. |
| <a id="mlmodelversionversion"></a>`version` | [`String!`](#string) | Name of the version. |

View File

@ -15,6 +15,9 @@ GitLab package.
## Prerequisites
- Create an [upgrade plan](../plan_your_upgrade.md).
We recommend upgrading in a test environment first and having a [rollback plan](../plan_your_upgrade.md#rollback-plan)
to reduce the risk of unplanned outages and extended downtime.
- Decide when to upgrade by viewing the [supported upgrade paths](../index.md#upgrade-paths).
You can't directly skip major versions (for example, go from 10.3 to 12.7 in one step).
- If you are upgrading from a non-package installation to a GitLab package installation, see

View File

@ -59,12 +59,11 @@ In the GitLab UI, GitLab Duo Chat knows about these areas:
In the IDEs, GitLab Duo Chat knows about these areas:
| Area | How to ask Chat |
|-------------------------------|-----------------------|
| Selected lines in the editor | With the lines selected, ask about `this code` or `this file`. Chat is not aware of the file; you must select the lines you want to ask about. |
| Epics | Ask about the URL. |
| Issues | Ask about the URL. |
| Open files | Start working in a file, making sure that you have opened the files you want as used as context. For details, see [open tabs as context](../project/repository/code_suggestions/index.md#open-tabs-as-context). |
| Area | How to ask Chat |
|---------|------------------|
| Selected lines in the editor | With the lines selected, ask about `this code` or `this file`. Chat is not aware of the file; you must select the lines you want to ask about. |
| Epics | Ask about the URL. |
| Issues | Ask about the URL. |
In addition, in the IDEs, when you use any of the slash commands,
like `/explain`, `/refactor`, `/fix`, or `/tests,` Duo Chat has access to the

View File

@ -104,9 +104,11 @@ you might write something like:
AI is non-deterministic, so you may not get the same suggestion every time with the same input.
To generate quality code, write clear, descriptive, specific tasks.
### Best practice examples
For use cases and best practices, follow the [GitLab Duo examples documentation](../../../gitlab_duo_examples.md).
## Open tabs as context
#### Use open tabs as context
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/464767) in GitLab 17.2 [with a flag](../../../../administration/feature_flags.md) named `advanced_context_resolver`. Disabled by default.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/462750) in GitLab 17.2 [with a flag](../../../../administration/feature_flags.md) named `code_suggestions_context`. Disabled by default.
@ -118,54 +120,30 @@ FLAG:
The availability of this feature is controlled by a feature flag.
For more information, see the history.
To get more accurate and relevant results from Code Suggestions and Code Generation, you can use
the contents of the files open in tabs in your IDE. Similar to prompt engineering, these files
For better results from GitLab Duo Code Suggestions, ensure that Open Tabs Context is enabled in your IDE settings.
This feature uses the contents of the files currently open in your IDE to get more
accurate and relevant results from Code Suggestions. Like prompt engineering, these files
give GitLab Duo more information about the standards and practices in your code project.
### Enable open tabs as context
To get the most benefit from using your open tabs as context, open the files relevant to the code
you want to create, including configuration files. When you start work in a new file,
Code Suggestions offers you suggestions in the new file.
Prerequisites:
- GitLab Duo Code Suggestions must be enabled for your project.
- For GitLab self-managed instances, the `code_suggestions_context`and the
`advanced_context_resolver` [feature flags](../../../feature_flags.md) must be enabled.
- Use a supported code language:
- Code Completion: All configured languages.
- Code Generation: Go, Java, JavaScript, Kotlin, Python, Ruby, Rust, TypeScript (`.ts` and `.tsx` files),
Vue, and YAML.
- Requires GitLab 17.2 and later. Earlier GitLab versions that support Code Suggestions
cannot weight the content of open tabs more heavily than other files in your project.
- Requires a [supported code language](#advanced-context-supported-languages).
::Tabs
1. Open the files you want to provide for context. Advanced Context uses the most recently
opened or changed files for context. If you dont want a file sent as additional context, close it.
1. To fine-tune your Code Generation results, add code comments to your file that explain
what you want to build. Code Generation treats your code comments like chat. Your code comments
update the `user_instruction`, and then improve the next results you receive.
:::TabTitle Visual Studio Code
1. Install the [GitLab Workflow extension version 4.14.2 or later](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow)
from the Visual Studio Marketplace.
1. Configure the [extension settings](https://gitlab.com/gitlab-org/gitlab-vscode-extension#extension-settings).
1. Enable the feature by turning on the `gitlab.aiAssistedCodeSuggestions.enabledSupportedLanguages` setting.
:::TabTitle JetBrains IDEs
For installation instructions for JetBrains IDEs, see the
[GitLab JetBrains Plugin documentation](https://gitlab.com/gitlab-org/editor-extensions/gitlab-jetbrains-plugin#toggle-sending-open-tabs-as-context).
::EndTabs
### Use open tabs as context
Open the files you want to provide for context:
- Open tabs uses the most recently opened or changed files.
- If you do not want a file used as additional context, close that file.
When you start working in a file, GitLab Duo uses your open files
as extra context, within [truncation limits](#truncation-of-file-content).
You can adjust your Code Generation results by adding code comments to your file
that explain what you want to build. Code Generation treats your code comments
like chat. Your code comments update the `user_instruction`, and then improve
the next results you receive.
### Related topics
As you work, GitLab Duo provides code suggestions that use your other open files
(within [truncation limits](#truncation-of-file-content))
as extra context.
To learn about the code that builds the prompt, see these files:
@ -173,11 +151,19 @@ To learn about the code that builds the prompt, see these files:
[`ee/lib/api/code_suggestions.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/api/code_suggestions.rb#L76)
in the `gitlab` repository.
- **Code Completion**:
[`ai_gateway/code_suggestions/processing/completions.py`](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/fcb3f485a8f047a86a8166aad81f93b6d82106a7/ai_gateway/code_suggestions/processing/completions.py#L273) in the `modelops` repository.
[`ai_gateway/code_suggestions/processing/completions.py`](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/fcb3f485a8f047a86a8166aad81f93b6d82106a7/ai_gateway/code_suggestions/processing/completions.py#L273)
in the `modelops` repository.
You can provide feedback about this feature in
We'd love your feedback about the Advanced Context feature in
[issue 258](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/issues/258).
### Advanced Context supported languages
The Advanced Context feature supports these languages:
- Code Completion: all configured languages.
- Code Generation: Go, Java, JavaScript, Kotlin, Python, Ruby, Rust, TypeScript (`.ts` and `.tsx` files), Vue, and YAML.
## Response time
Code Suggestions is powered by a generative AI model.

View File

@ -141,6 +141,57 @@ To do this:
::EndTabs
## Use open tabs as context
> - [Introduced](https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/issues/206) in GitLab 17.2.
To enhance the accuracy and relevance of GitLab Duo Code Suggestions, enable the use of
open tabs as context in your IDE settings. This feature uses the contents of files most recently
opened or changed in your IDE to provide more tailored code suggestions, within certain truncation limits.
This extra context gives you:
- More accurate and relevant code suggestions
- Better alignment with your project's standards and practices
- Improved context for new file creation
Open tabs as context supports these languages:
- Code Completion: All configured languages.
- Code Generation: Go, Java, JavaScript, Kotlin, Python, Ruby, Rust, TypeScript (`.ts` and `.tsx` files),
Vue, and YAML.
## Enable open tabs as context
Prerequisites:
- Requires GitLab 17.1 or later.
- For GitLab self-managed instances, enable the `code_suggestions_context`and the
`advanced_context_resolver` [feature flags](../../../feature_flags.md).
- GitLab Duo Code Suggestions enabled for your project
- For Visual Studio Code, requires the GitLab Workflow extension, version 4.14.2 or later.
::Tabs
:::TabTitle Visual Studio Code
1. Install the [GitLab Workflow extension](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow)
from the Visual Studio Marketplace.
1. Configure the extension following the
[setup instructions](https://gitlab.com/gitlab-org/gitlab-vscode-extension#extension-settings).
1. Enable the feature by toggling the `gitlab.aiAssistedCodeSuggestions.enabledSupportedLanguages` setting.
:::TabTitle JetBrains IDEs
For installation instructions for JetBrains IDEs, see the
[GitLab JetBrains Plugin documentation](https://gitlab.com/gitlab-org/editor-extensions/gitlab-jetbrains-plugin#toggle-sending-open-tabs-as-context).
::EndTabs
When you're ready to start coding:
1. Open relevant files, including configuration files, to provide better context.
1. Close any files you don't want to be used as context.
## View multiple code suggestions
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/issues/1325) in GitLab 17.1.

View File

@ -28,10 +28,6 @@ module API
bad_request!(WEB_BROWSER_ERROR_MESSAGE) if MAJOR_BROWSERS.any? { |b| browser.method(:"#{b}?").call }
end
def require_dependency_proxy_enabled!
not_found! unless ::Gitlab.config.dependency_proxy.enabled
end
def send_successful_response_from(service_response:)
action, action_params = service_response.to_h.values_at(:action, :action_params)
case action

View File

@ -0,0 +1,141 @@
# frozen_string_literal: true
module API
module Concerns
module VirtualRegistries
module Packages
module Maven
module RegistryEndpoints
extend ActiveSupport::Concern
included do
desc 'Get the list of all maven virtual registries' do
detail 'This feature was introduced in GitLab 17.4. \
This feature is currently in an experimental state. \
This feature is behind the `virtual_registry_maven` feature flag.'
success ::API::Entities::VirtualRegistries::Packages::Maven::Registry
failure [
{ code: 400, message: 'Bad Request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' }
]
tags %w[maven_virtual_registries]
hidden true
end
params do
requires :group_id, type: Integer, desc: 'The ID of the group', allow_blank: false
end
get do
group = find_group!(declared_params[:group_id])
authorize! :read_virtual_registry, ::VirtualRegistries::Packages::Policies::Group.new(group)
registries = ::VirtualRegistries::Packages::Maven::Registry.for_group(group)
present registries, with: ::API::Entities::VirtualRegistries::Packages::Maven::Registry
end
desc 'Create a new maven virtual registry' do
detail 'This feature was introduced in GitLab 17.4. \
This feature is currently in an experimental state. \
This feature is behind the `virtual_registry_maven` feature flag.'
success code: 201
failure [
{ code: 400, message: 'Bad request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' }
]
tags %w[maven_virtual_registries]
hidden true
end
params do
requires :group_id, type: Integer, desc: 'The ID of the group. Must be a top-level group',
allow_blank: false
optional :cache_validity_hours, type: Integer, desc: 'The validity of the cache in hours. Defaults to 1'
end
post do
group = find_group!(declared_params[:group_id])
authorize! :create_virtual_registry, ::VirtualRegistries::Packages::Policies::Group.new(group)
new_reg = ::VirtualRegistries::Packages::Maven::Registry.new(declared_params(include_missing: false))
render_validation_error!(new_reg) unless new_reg.save
created!
end
route_param :id, type: Integer, desc: 'The ID of the maven virtual registry' do
desc 'Get a specific maven virtual registry' do
detail 'This feature was introduced in GitLab 17.4. \
This feature is currently in an experimental state. \
This feature is behind the `virtual_registry_maven` feature flag.'
success ::API::Entities::VirtualRegistries::Packages::Maven::Registry
failure [
{ code: 400, message: 'Bad request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' }
]
tags %w[maven_virtual_registries]
hidden true
end
get do
authorize! :read_virtual_registry, registry
present registry, with: ::API::Entities::VirtualRegistries::Packages::Maven::Registry
end
desc 'Update a specific maven virtual registry' do
detail 'This feature was introduced in GitLab 17.4. \
This feature is currently in an experimental state. \
This feature is behind the `virtual_registry_maven` feature flag.'
success ::API::Entities::VirtualRegistries::Packages::Maven::Registry
failure [
{ code: 400, message: 'Bad request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' }
]
tags %w[maven_virtual_registries]
hidden true
end
params do
requires :cache_validity_hours, type: Integer, desc: 'The validity of the cache in hours'
end
patch do
authorize! :update_virtual_registry, registry
render_validation_error!(registry) unless registry.update(declared_params)
present registry, with: ::API::Entities::VirtualRegistries::Packages::Maven::Registry
end
desc 'Delete a specific maven virtual registry' do
detail 'This feature was introduced in GitLab 17.4. \
This feature is currently in an experimental state. \
This feature is behind the `virtual_registry_maven` feature flag.'
success code: 204
failure [
{ code: 400, message: 'Bad request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' },
{ code: 412, message: 'Precondition Failed' }
]
tags %w[maven_virtual_registries]
hidden true
end
delete do
authorize! :destroy_virtual_registry, registry
destroy_conditionally!(registry)
end
end
end
end
end
end
end
end
end

View File

@ -10,7 +10,7 @@ module API
included do
desc 'List all maven virtual registry upstreams' do
detail 'This feature was introduced in GitLab 17.3. \
detail 'This feature was introduced in GitLab 17.4. \
This feature is currently in experiment state. \
This feature behind the `virtual_registry_maven` feature flag.'
success code: 200
@ -30,7 +30,7 @@ module API
end
desc 'Add a maven virtual registry upstream' do
detail 'This feature was introduced in GitLab 17.3. \
detail 'This feature was introduced in GitLab 17.4. \
This feature is currently in experiment state. \
This feature behind the `virtual_registry_maven` feature flag.'
success code: 201
@ -68,7 +68,7 @@ module API
route_param :upstream_id, type: Integer, desc: 'The ID of the maven virtual registry upstream' do
desc 'Get a specific maven virtual registry upstream' do
detail 'This feature was introduced in GitLab 17.3. \
detail 'This feature was introduced in GitLab 17.4. \
This feature is currently in experiment state. \
This feature behind the `virtual_registry_maven` feature flag.'
success code: 200
@ -90,7 +90,7 @@ module API
end
desc 'Update a maven virtual registry upstream' do
detail 'This feature was introduced in GitLab 17.3. \
detail 'This feature was introduced in GitLab 17.4. \
This feature is currently in experiment state. \
This feature behind the `virtual_registry_maven` feature flag.'
success code: 200
@ -121,7 +121,7 @@ module API
end
desc 'Delete a maven virtual registry upstream' do
detail 'This feature was introduced in GitLab 17.3. \
detail 'This feature was introduced in GitLab 17.4. \
This feature is currently in experiment state. \
This feature behind the `virtual_registry_maven` feature flag.'
success code: 204

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module API
module Entities
module VirtualRegistries
module Packages
module Maven
class Registry < Grape::Entity
expose :id, :group_id, :cache_validity_hours, :created_at, :updated_at
end
end
end
end
end
end

View File

@ -50,8 +50,6 @@ module API
end
def update_last_used_at!
return if Feature.enabled?(:disable_ssh_key_used_tracking)
key&.update_last_used_at
end

View File

@ -47,6 +47,8 @@ module API
namespace 'virtual_registries/packages/maven' do
namespace :registries do
include ::API::Concerns::VirtualRegistries::Packages::Maven::RegistryEndpoints
route_param :id, type: Integer, desc: 'The ID of the maven virtual registry' do
namespace :upstreams do
include ::API::Concerns::VirtualRegistries::Packages::Maven::UpstreamEndpoints

View File

@ -136,8 +136,12 @@ module Gitlab
# in the best case, where all 100 changes belong to the same project, we'll execute
# a single update statement.
def execute
changes_by_project.each do |changes|
connection.execute(update_sql(changes))
::Gitlab::Database.allow_cross_joins_across_databases(
url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/478765'
) do
changes_by_project.each do |changes|
connection.execute(update_sql(changes))
end
end
end
@ -282,20 +286,24 @@ module Gitlab
def perform
user_id = Users::Internal.security_bot.id
each_sub_batch do |sub_batch|
cte = Gitlab::SQL::CTE.new(:batched_relation, sub_batch.limit(100))
::Gitlab::Database.allow_cross_joins_across_databases(
url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/478765'
) do
each_sub_batch do |sub_batch|
cte = Gitlab::SQL::CTE.new(:batched_relation, sub_batch.limit(100))
filtered_batch = cte
.apply_to(Migratable::Vulnerabilities::Read.all)
.joins('INNER JOIN vulnerability_scanners ON vulnerability_scanners.id = vulnerability_reads.scanner_id')
.where('vulnerability_scanners.external_id': REMOVED_SCANNERS.keys)
filtered_batch = cte
.apply_to(Migratable::Vulnerabilities::Read.all)
.joins('INNER JOIN vulnerability_scanners ON vulnerability_scanners.id = vulnerability_reads.scanner_id')
.where('vulnerability_scanners.external_id': REMOVED_SCANNERS.keys)
vulnerability_tuples = values_for_fields(
filtered_batch, :vulnerability_id, 'vulnerability_reads.project_id', :namespace_id, :severity, :uuid
)
vulnerability_tuples = values_for_fields(
filtered_batch, :vulnerability_id, 'vulnerability_reads.project_id', :namespace_id, :severity, :uuid
)
connection.transaction do
perform_bulk_writes(user_id, vulnerability_tuples)
connection.transaction do
perform_bulk_writes(user_id, vulnerability_tuples)
end
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Gitlab
module Middleware
class ActionControllerStaticContext
def initialize(app)
@app = app
end
def call(env)
return @app.call(env) unless Feature.enabled?(:controller_static_context, Feature.current_request)
req = ActionDispatch::Request.new(env)
action_name = req.path_parameters[:action]
caller_id = req.controller_class.try(:endpoint_id_for_action, action_name)
feature_category = req.controller_class.try(:feature_category_for_action, action_name).to_s
context = {}
context[:caller_id] = caller_id if caller_id
context[:feature_category] = feature_category if feature_category.present?
# We need to push the context here, so it persists after this middleware finishes
# We need the values present in Labkit's rack middleware that surrounds this one.
Gitlab::ApplicationContext.push(context)
@app.call(env)
end
end
end
end

View File

@ -47,7 +47,7 @@ namespace :tw do
CodeOwnerRule.new('Dynamic Analysis', '@rdickenson @phillipwells'),
CodeOwnerRule.new('Editor Extensions', '@aqualls'),
CodeOwnerRule.new('Environments', '@phillipwells'),
CodeOwnerRule.new('Foundations', '@sselhorn'),
CodeOwnerRule.new('Personal Productivity', '@sselhorn'),
# CodeOwnerRule.new('Fulfillment Platform', ''),
CodeOwnerRule.new('Fuzz Testing', '@rdickenson'),
CodeOwnerRule.new('Geo', '@axil'),
@ -105,7 +105,7 @@ namespace :tw do
CodeOwnerRule.new('Distribution', '@gitlab-org/distribution'),
CodeOwnerRule.new('Documentation Guidelines', '@sselhorn'),
CodeOwnerRule.new('Engineering Productivity', '@gl-quality/eng-prod'),
CodeOwnerRule.new('Foundations', '@gitlab-org/manage/foundations/engineering'),
CodeOwnerRule.new('Personal Productivity', '@gitlab-org/foundations/engineering'),
CodeOwnerRule.new('Gitaly', '@proglottis @toon'),
CodeOwnerRule.new('Global Search', '@gitlab-org/search-team/migration-maintainers'),
CodeOwnerRule.new('Remote Development',

View File

@ -56956,9 +56956,6 @@ msgstr ""
msgid "Unauthorized to update the environment"
msgstr ""
msgid "Unavailable"
msgstr ""
msgid "Unban"
msgstr ""

View File

@ -483,3 +483,11 @@ function define_trigger_branch_in_build_env() {
echo "TRIGGER_BRANCH=${TRIGGER_BRANCH}" >> $BUILD_ENV
fi
}
function log_disk_usage() {
echo -e "df -h"
df -h
echo -e "\n\nls -lhS tmp"
ls -lhS tmp
}

View File

@ -945,12 +945,6 @@ RSpec.describe ApplicationController, feature_category: :shared do
expect(json_response['meta.project']).to eq(project.full_path)
end
it 'sets the caller_id as controller#action' do
get :index, format: :json
expect(json_response['meta.caller_id']).to eq('AnonymousController#index')
end
it 'sets the feature_category as defined in the controller' do
get :index, format: :json

View File

@ -41,17 +41,6 @@ RSpec.describe ConfirmationsController, feature_category: :system_access do
expect(response.body).not_to include(user.email)
end
it 'sets the username and caller_id in the context' do
expect(controller).to receive(:show).and_wrap_original do |m, *args|
m.call(*args)
expect(Gitlab::ApplicationContext.current)
.to include('meta.user' => user.username, 'meta.caller_id' => 'ConfirmationsController#show')
end
perform_request
end
end
context 'user accesses the link after the expiry of confirmation token has passed' do
@ -76,17 +65,6 @@ RSpec.describe ConfirmationsController, feature_category: :system_access do
expect(response.body).not_to include(user.email)
end
it 'sets the username and caller_id in the context' do
expect(controller).to receive(:show).and_wrap_original do |m, *args|
m.call(*args)
expect(Gitlab::ApplicationContext.current)
.to include('meta.user' => user.username, 'meta.caller_id' => 'ConfirmationsController#show')
end
travel_to(3.days.from_now) { perform_request }
end
end
context 'with an invalid confirmation token' do
@ -103,17 +81,6 @@ RSpec.describe ConfirmationsController, feature_category: :system_access do
expect(response.body).to include('Confirmation token is invalid')
end
it 'sets the the caller_id in the context' do
expect(controller).to receive(:show).and_wrap_original do |m, *args|
expect(Gitlab::ApplicationContext.current)
.to include('meta.caller_id' => 'ConfirmationsController#show')
m.call(*args)
end
perform_request
end
end
end

View File

@ -467,19 +467,6 @@ RSpec.describe OmniauthCallbacksController, type: :controller, feature_category:
expect(request.env['warden']).to be_authenticated
end
it 'sets the username and caller_id in the context' do
expect(controller).to receive(:atlassian_oauth2).and_wrap_original do |m, *args|
m.call(*args)
expect(Gitlab::ApplicationContext.current).to include(
'meta.user' => user.username,
'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2'
)
end
post :atlassian_oauth2
end
context 'when a user has 2FA enabled' do
let(:user) { create(:atlassian_user, :two_factor, extern_uid: extern_uid) }
@ -742,19 +729,6 @@ RSpec.describe OmniauthCallbacksController, type: :controller, feature_category:
expect { post :saml, params: { SAMLResponse: mock_saml_response } }.not_to change { user.identities.count }
end
it 'sets the username and caller_id in the context' do
expect(controller).to receive(:saml).and_wrap_original do |m, *args|
m.call(*args)
expect(Gitlab::ApplicationContext.current).to include(
'meta.user' => user.username,
'meta.caller_id' => 'OmniauthCallbacksController#saml'
)
end
post :saml, params: { SAMLResponse: mock_saml_response }
end
context 'with IDP bypass two factor request' do
let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml') }

View File

@ -83,17 +83,6 @@ RSpec.describe PasswordsController, feature_category: :system_access do
)
end
end
it 'sets the username and caller_id in the context' do
expect(controller).to receive(:update).and_wrap_original do |m, *args|
m.call(*args)
expect(Gitlab::ApplicationContext.current)
.to include('meta.user' => user.username, 'meta.caller_id' => 'PasswordsController#update')
end
subject
end
end
end

View File

@ -198,6 +198,16 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state, featu
end
end
it "avoids N+1 database queries", :use_sql_query_cache do
get_show_json
control = ActiveRecord::QueryRecorder.new(skip_cached: false) { get_show_json }
create_list(:ci_build, 5, :failed, pipeline: pipeline)
expect { get_show_json }.to issue_same_number_of_queries_as(control)
end
context 'when job is running' do
before do
get_show_json

View File

@ -505,17 +505,6 @@ RSpec.describe RegistrationsController, feature_category: :user_profile do
expect(User.last.name).to eq full_name(base_user_params[:first_name], base_user_params[:last_name])
end
it 'sets the caller_id in the context' do
expect(controller).to receive(:create).and_wrap_original do |m, *args|
m.call(*args)
expect(Gitlab::ApplicationContext.current)
.to include('meta.caller_id' => 'RegistrationsController#create')
end
subject
end
context 'when the password is weak' do
render_views
let_it_be(:new_user_params) { { new_user: base_user_params.merge({ password: "password" }) } }
@ -718,16 +707,5 @@ RSpec.describe RegistrationsController, feature_category: :user_profile do
expect_failure(s_('Profiles|You must accept the Terms of Service in order to perform this action.'))
end
end
it 'sets the username and caller_id in the context' do
expect(controller).to receive(:destroy).and_wrap_original do |m, *args|
m.call(*args)
expect(Gitlab::ApplicationContext.current)
.to include('meta.user' => user.username, 'meta.caller_id' => 'RegistrationsController#destroy')
end
post :destroy
end
end
end

View File

@ -704,61 +704,6 @@ RSpec.describe SessionsController, feature_category: :system_access do
end
end
describe '#set_current_context' do
let_it_be(:user) { create(:user) }
context 'when signed in' do
before do
sign_in(user)
end
it 'sets the username and caller_id in the context' do
expect(controller).to receive(:destroy).and_wrap_original do |m, *args|
expect(Gitlab::ApplicationContext.current)
.to include('meta.user' => user.username, 'meta.caller_id' => 'SessionsController#destroy')
m.call(*args)
end
delete :destroy
end
end
context 'when not signed in' do
it 'sets the caller_id in the context' do
expect(controller).to receive(:new).and_wrap_original do |m, *args|
expect(Gitlab::ApplicationContext.current)
.to include('meta.caller_id' => 'SessionsController#new')
expect(Gitlab::ApplicationContext.current)
.not_to include('meta.user')
m.call(*args)
end
get :new
end
end
context 'when the user becomes locked' do
before do
user.update!(failed_attempts: User.maximum_attempts.pred)
end
it 'sets the caller_id in the context' do
allow_any_instance_of(User).to receive(:lock_access!).and_wrap_original do |m, *args|
expect(Gitlab::ApplicationContext.current)
.to include('meta.caller_id' => 'SessionsController#create')
expect(Gitlab::ApplicationContext.current)
.not_to include('meta.user')
m.call(*args)
end
post :create, params: { user: { login: user.username, password: user.password.succ } }
end
end
end
describe '#destroy' do
before do
sign_in(user)

View File

@ -0,0 +1,58 @@
# ***********************************************************************************************************
# Tests in this folder are recommended to run locally to test ci configuration changes
# The tests are slow due to the yaml size and the amount of nodes to process.
# The specific bottleneck is at Gitlab::Ci::Pipeline::Chain::seed #perform!
# ***********************************************************************************************************
# TEST COVERAGE (Please keep this file updated)
#
# 1. Branch pipelines
# GitLab.com gitlab-org/gitlab-foss Project
# Master Pipeline Triggered by Push
# Scheduled Master Pipeline
# Scheduled ruby3_2 branch scheduled pipeline
# Stable branch pipeline
#
# GitLab.com gitlab-org/gitlab Master Pipeline
# Default Master Pipeline Triggered by Push
# Scheduled pipeline - Nightly
# Scheduled pipeline - Maintenance
#
# GitLab.com gitlab-org/security/gitlab
# Default Master Pipeline
# Scheduled pipeline - Nightly
# Auto-deploy branch pipeline
#
# 2. Merge Request Pipeline (and merge train pipeline under the same contexts)
# Unlabeled MR Changing GITALY_SERVER_VERSION
# Unlabeled MR Changing Dangerfile, .gitlab/ci/frontend.gitlab-ci.yml
# Unlabeled MR targeting stable branch Changing app/model/user.rb
#
# MR Labeled with pipeline:run-all-rspec Changing app/models/user.rb
# MR Labeled with pipeline:expedite pipeline::expedited Changing app/models/user.rb
# MR Labeled with pipeline::tier-1 Changing app/models/user.rb
# MR Labeled with pipeline::tier-2 and pipeline:mr-approved Changing app/models/user.rb
# MR Labeled with pipeline::tier-3 and pipeline:mr-approved Changing app/models/user.rb
# MR Labeled with pipeline:run-as-if-foss Changing app/models/user.rb
# MR Labeled with pipeline:force-run-as-if-jh Changing app/models/user.rb
# MR Labeled with pipeline:run-as-if-jh and pipeline:mr-approved Changing app/models/user.rb
# MR Labeled with pipeline:run-in-ruby3_2 Changing app/models/user.rb
#
# Automated MR
# MR Created from release-tools/update-gitaly Source Branch Changing GITALY_SERVER_VERSION
#
# Stable Branch
# MR Targeting a Stable Branch Changing app/models/user.rb
#
# Fork Project MRs
# MR Created from a Fork Project Master Branch Changing package.json
# MR Created from a Fork Project Feature Branch Changing package.json
# ***********************************************************************************************************
# CONTRIBUTE
#
# If you think we are missing coverage for an important pipeline type, please add them.
#
# For example, ci rule changes could break gitlab-foss pipelines, as seen in
# https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/7356
# we then added test cases by simulating pipeline for gitlab-org/gitlab-foss
# See `with gitlab.com gitlab-org gitlab-foss project` context below for details.
# ***********************************************************************************************************

View File

@ -0,0 +1,170 @@
# frozen_string_literal: true
require 'spec_helper'
require_relative './shared_context_and_examples'
RSpec.describe 'CI configuration validation - branch pipelines', feature_category: :tooling do
include ProjectForksHelper
include CiConfigurationValidationHelper
include_context 'with simulated pipeline attributes and shared project and user'
let(:pipeline_project) { gitlab_org_gitlab_project }
let(:create_pipeline_service) do
Ci::CreatePipelineService.new(pipeline_project, user, ref: ci_commit_branch)
end
let(:content) do
pipeline_project.repository.blob_at(ci_commit_branch, '.gitlab-ci.yml').data
end
subject(:pipeline) do
trigger_source = ci_pipeline_source.to_sym
create_pipeline_service
.execute(trigger_source, dry_run: true, content: content, variables_attributes: variables_attributes)
.payload
end
context 'with branch pipelines' do
let(:ci_commit_branch) { master_branch }
let(:variables_attributes) do
[
*variables_attributes_base,
{ key: 'CI_COMMIT_BRANCH', value: ci_commit_branch },
{ key: 'CI_COMMIT_REF_NAME', value: ci_commit_branch }
]
end
context 'with gitlab.com gitlab-org/gitlab master pipeline' do
context 'with default master pipeline' do
let(:expected_job_name) { 'db:migrate:multi-version-upgrade' }
it_behaves_like 'default branch pipeline'
end
context 'with scheduled nightly' do
let(:ci_pipeline_source) { 'schedule' }
let(:expected_job_name) { 'db:rollback single-db' }
let(:variables_attributes) do
super() << { key: 'SCHEDULE_TYPE', value: 'nightly' }
end
it_behaves_like 'default branch pipeline'
end
context 'with scheduled maintenance' do
let(:ci_pipeline_source) { 'schedule' }
let(:expected_job_name) { 'generate-frontend-fixtures-mapping' }
let(:variables_attributes) do
super() << { key: 'SCHEDULE_TYPE', value: 'maintenance' }
end
it_behaves_like 'default branch pipeline'
end
end
context 'with gitlab.com gitlab-org/gitlab ruby3_2 branch scheduled pipeline' do
let(:ci_commit_branch) { 'ruby3_2' }
let(:ci_pipeline_source) { 'schedule' }
let(:expected_job_name) { 'ruby_syntax: [${RUBY_VERSION_DEFAULT}]' }
let(:variables_attributes) do
super() << { key: 'SCHEDULE_TYPE', value: 'nightly' }
end
before do
sync_local_files_to_project(
gitlab_org_gitlab_project,
user,
ci_commit_branch,
files: ci_glob_with_common_file_globs
)
end
it_behaves_like 'default branch pipeline'
end
context 'with gitlab.com gitlab-org/gitlab stable branch pipeline' do
let(:ci_commit_branch) { '17-1-stable-ee' }
let(:expected_job_name) { 'run-dev-fixtures-ee' }
subject(:pipeline) do
trigger_source = ci_pipeline_source.to_sym
create_pipeline_service
.execute(trigger_source, dry_run: true, content: content, variables_attributes: variables_attributes)
.payload
end
before do
sync_local_files_to_project(
pipeline_project,
user,
ci_commit_branch,
files: ci_glob_with_common_file_globs
)
end
it_behaves_like 'default branch pipeline'
end
context 'with fork project' do
let(:ci_commit_branch) { master_branch }
before do
pipeline_project.add_developer(user)
sync_local_files_to_project(
pipeline_project,
user,
ci_commit_branch,
files: ci_glob_with_common_file_globs
)
end
context 'with gitlab.com gitlab-org/security/gitlab project' do
let_it_be(:sub_group) { create(:group, parent: group, path: 'security') }
let_it_be(:security_project) { create(:project, :empty_repo, group: sub_group, path: 'gitlab') }
let(:ci_project_namespace) { 'gitlab-org/security' }
let(:pipeline_project) { security_project }
context 'when master pipeline is triggered by push' do
let(:expected_job_name) { 'static-verification-with-database' }
it_behaves_like 'default branch pipeline'
end
context 'with scheduled master pipeline' do
let(:ci_pipeline_source) { 'schedule' }
let(:expected_job_name) { 'update-ruby-gems-coverage-cache-push' }
it_behaves_like 'default branch pipeline'
end
context 'with auto-deploy branch pipeilne' do
let(:ci_commit_branch) { '17-3-auto-deploy-2024080508' }
let(:expected_job_name) { 'build-qa-image' }
it_behaves_like 'default branch pipeline'
end
end
context 'with gitlab.com gitlab-org gitlab-foss project' do
let_it_be(:foss_project) { create(:project, :empty_repo, group: group, path: 'gitlab-foss') }
let(:pipeline_project) { foss_project }
context 'with master pipeline triggered by push' do
let(:expected_job_name) { 'db:backup_and_restore single-db' }
it_behaves_like 'default branch pipeline'
end
context 'with scheduled master pipeline' do
let(:ci_pipeline_source) { 'schedule' }
let(:expected_job_name) { 'db:backup_and_restore single-db' }
it_behaves_like 'default branch pipeline'
end
end
end
end
end

View File

@ -0,0 +1,113 @@
# frozen_string_literal: true
require 'spec_helper'
require_relative './shared_context_and_examples'
RSpec.describe 'CI configuration validation - branch pipelines', feature_category: :tooling do
include ProjectForksHelper
include CiConfigurationValidationHelper
include_context 'with simulated pipeline attributes and shared project and user'
include_context 'with simulated MR pipeline attributes'
let(:pipeline_project) { gitlab_org_gitlab_project }
let(:create_pipeline_service) { Ci::CreatePipelineService.new(target_project, user, ref: target_branch) }
subject(:pipeline) do
create_pipeline_service
.execute(
:push,
dry_run: true,
merge_request: merge_request,
variables_attributes: mr_pipeline_variables_attributes
).payload
end
context "when unlabeled MR is changing GITALY_SERVER_VERSION" do
let(:changed_files) { ['GITALY_SERVER_VERSION'] }
let(:expected_job_name) { 'eslint' }
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
context 'when MR is created from "release-tools/update-gitaly" source branch' do
let(:source_branch) { 'release-tools/update-gitaly' }
let(:changed_files) { ['GITALY_SERVER_VERSION'] }
let(:expected_job_name) { 'update-gitaly-binaries-cache' }
it_behaves_like 'merge request pipeline'
end
context 'when MR targeting a stable branch is changing app/models/user.rb' do
let(:target_branch) { '16-10-stable-ee' }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'rspec-all frontend_fixture 1/7' }
before do
sync_local_files_to_project(
target_project,
user,
target_branch,
files: ci_glob
)
end
after do
target_project.repository.delete_branch(target_branch)
end
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
context 'with fork project MRs' do
let_it_be(:fork_project_mr_source) { fork_project(gitlab_org_gitlab_project, user, repository: true) }
let(:source_project) { fork_project_mr_source }
let(:target_project) { gitlab_org_gitlab_project }
context 'when MR is created from a fork project master branch' do
let(:source_branch) { master_branch }
let(:target_branch) { master_branch }
let(:changed_files) { ['package.json'] }
let(:expected_job_name) { 'rspec-all frontend_fixture 1/7' }
context 'when running MR pipeline in the context of the fork project' do
let(:ci_project_namespace) { fork_project_mr_source.namespace.full_path }
let(:ci_project_path) { fork_project_mr_source.full_path }
let(:ci_project_name) { fork_project_mr_source.name }
it_behaves_like 'merge request pipeline'
end
context 'when running MR pipeline in the context of canonical project' do
it_behaves_like 'merge request pipeline'
end
it_behaves_like 'merge train pipeline'
end
context 'when MR is created from a fork project feature branch' do
let(:source_branch) { "feature_branch_ci_#{SecureRandom.uuid}" }
let(:target_branch) { master_branch }
let(:changed_files) { ['package.json'] }
let(:expected_job_name) { 'rspec-all frontend_fixture 1/7' }
context 'when running MR pipeline in the context of the fork project' do
let(:ci_project_namespace) { fork_project_mr_source.namespace.full_path }
let(:ci_project_path) { fork_project_mr_source.full_path }
let(:ci_project_name) { fork_project_mr_source.name }
it_behaves_like 'merge request pipeline'
end
context 'when running MR pipeline in the context of canonical project' do
it_behaves_like 'merge request pipeline'
end
it_behaves_like 'merge train pipeline'
end
end
end

View File

@ -0,0 +1,123 @@
# frozen_string_literal: true
require 'spec_helper'
require_relative './shared_context_and_examples'
RSpec.describe 'CI configuration validation - branch pipelines', feature_category: :tooling do
include ProjectForksHelper
include CiConfigurationValidationHelper
include_context 'with simulated pipeline attributes and shared project and user'
include_context 'with simulated MR pipeline attributes'
let(:pipeline_project) { gitlab_org_gitlab_project }
let(:create_pipeline_service) { Ci::CreatePipelineService.new(target_project, user, ref: target_branch) }
subject(:pipeline) do
create_pipeline_service
.execute(
:push,
dry_run: true,
merge_request: merge_request,
variables_attributes: mr_pipeline_variables_attributes
).payload
end
context 'when MR labeled with `pipeline:run-all-rspec` is changing app/models/user.rb' do
let(:mr_labels) { ['pipeline:run-all-rspec'] }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'rspec-all frontend_fixture 1/7' }
it_behaves_like 'merge request pipeline'
end
context 'when MR labeled with `pipeline:expedite pipeline::expedited` is changing app/models/user.rb' do
let(:mr_labels) { ['pipeline:expedite', 'pipeline::expedited'] }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'setup-test-env' }
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
context 'when MR labeled with `pipeline::tier-1`' do
let(:mr_labels) { ['pipeline::tier-1'] }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'pipeline-tier-1' }
it_behaves_like 'merge request pipeline'
end
context 'when MR labeled with `pipeline::tier-2` and `pipeline:mr-approved`' do
let(:mr_labels) { ['pipeline::tier-2', 'pipeline:mr-approved'] }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'pipeline-tier-2' }
it_behaves_like 'merge request pipeline'
end
context 'when MR labeled with `pipeline::tier-3` and `pipeline:mr-approved`' do
let(:mr_labels) { ['pipeline::tier-3', 'pipeline:mr-approved'] }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'pipeline-tier-3' }
it_behaves_like 'merge request pipeline'
end
context 'when MR labeled with `pipeline:run-as-if-foss` is changing app/models/user.rb' do
let(:mr_labels) { ['pipeline:run-as-if-foss'] }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'start-as-if-foss' }
let(:mr_pipeline_variables_attributes) do
super() << { key: 'AS_IF_FOSS_TOKEN', value: 'foss token' }
end
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
context 'when MR labeled with `pipeline:force-run-as-if-jh` is changing app/models/user.rb' do
let(:mr_labels) { ['pipeline:force-run-as-if-jh'] }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'start-as-if-jh' }
let(:mr_pipeline_variables_attributes) do
# workflow rule for "include", see .gitlab-ci.yml
super() << { key: 'CI_PROJECT_URL', value: 'https://gitlab.com/gitlab-org/gitlab' }
end
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
context 'when MR labeled with `pipeline:run-as-if-jh` and `pipeline:mr-approved` is changing app/models/user.rb' do
let(:mr_labels) { ['pipeline:run-as-if-jh', 'pipeline:mr-approved'] }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'start-as-if-jh' }
let(:mr_pipeline_variables_attributes) do
base_attributes = super()
# workflow rule for "include", see .gitlab-ci.yml
base_attributes << { key: 'CI_PROJECT_URL', value: 'https://gitlab.com/gitlab-org/gitlab' }
base_attributes << { key: 'CI_AS_IF_JH_ENABLED', value: 'true' }
end
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
context 'when MR labeled with `pipeline:run-in-ruby3_2` is changing app/models/user.rb' do
let(:mr_labels) { ['pipeline:run-in-ruby3_2'] }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'e2e-test-pipeline-generate' }
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
end

View File

@ -0,0 +1,174 @@
# frozen_string_literal: true
RSpec.shared_context 'with simulated pipeline attributes and shared project and user' do
let(:ci_server_host) { 'gitlab.com' }
let(:ci_project_namespace) { 'gitlab-org' }
let(:ci_project_path) { "#{ci_project_namespace}/#{ci_project_name}" }
let(:ci_project_name) { 'gitlab' }
let(:ci_pipeline_source) { 'push' }
let(:variables_attributes_base) do
[
{ key: 'CI_SERVER_HOST', value: ci_server_host },
{ key: 'CI_PROJECT_NAMESPACE', value: ci_project_namespace },
{ key: 'CI_PROJECT_PATH', value: ci_project_path },
{ key: 'CI_PROJECT_NAME', value: ci_project_name },
{ key: 'CI_PIPELINE_SOURCE', value: ci_pipeline_source },
{ key: 'GITLAB_INTERNAL', value: 'true' }
]
end
let(:jobs) { pipeline.stages.flat_map { |s| s.statuses.map(&:name) } }
let_it_be(:group) { create(:group, path: 'gitlab-org') }
let_it_be(:gitlab_org_gitlab_project) { create(:project, :empty_repo, group: group, path: 'gitlab') }
let_it_be(:user) { create(:user) }
let_it_be(:ci_glob) { Dir.glob("{.gitlab-ci.yml,.gitlab/**/*.yml}").freeze }
let_it_be(:ci_glob_with_common_file_globs) { [*ci_glob, 'lib/api/lint.rb', 'doc/index.md'] }
let_it_be(:master_branch) { 'master' }
around do |example|
with_net_connect_allowed { example.run } # creating pipeline requires network call to fetch templates
end
before_all do
gitlab_org_gitlab_project.add_developer(user)
sync_local_files_to_project(
gitlab_org_gitlab_project,
user,
master_branch,
files: ci_glob_with_common_file_globs
)
end
before do
# delete once we have a migration to permanently increase limit
stub_application_setting(max_yaml_size_bytes: 2.megabytes)
end
end
RSpec.shared_context 'with simulated MR pipeline attributes' do
let(:ci_pipeline_source) { 'merge_request_event' }
let(:ci_merge_request_event_type) { 'merged_result' }
let(:mr_labels) { [] }
let(:mr_pipeline_variables_attributes) do
[
*variables_attributes_base,
{ key: 'CI_COMMIT_REF_NAME', value: source_branch },
{ key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: ci_merge_request_event_type }
]
end
let(:create_pipeline_service) { Ci::CreatePipelineService.new(target_project, user, ref: target_branch) }
let(:source_project) { gitlab_org_gitlab_project }
let(:target_project) { gitlab_org_gitlab_project }
let(:source_branch) { "feature_branch_ci_#{SecureRandom.uuid}" }
let(:target_branch) { master_branch }
let(:merge_request) do
create(:labeled_merge_request,
source_project: source_project,
source_branch: source_branch,
target_project: target_project,
target_branch: target_branch,
labels: mr_labels.map { |label_title| create(:label, title: label_title) }
)
end
before do
file_change_actions = changed_files.map do |file_path|
action = source_project.repository.blob_at(source_branch, file_path).nil? ? :create : :update
{
action: action,
file_path: file_path,
content: 'content'
}
end
source_project.repository.commit_files(
user,
branch_name: source_branch,
message: 'changes files',
actions: file_change_actions
)
end
after do
source_project.repository.delete_branch(source_branch)
end
end
RSpec.shared_examples 'default branch pipeline' do
it 'is valid' do
expect(pipeline.yaml_errors).to be nil
expect(pipeline.status).to eq('created')
expect(jobs).to include(expected_job_name)
end
end
RSpec.shared_examples 'merge request pipeline' do
it "succeeds with expected job" do
expect(pipeline.yaml_errors).to be nil
expect(pipeline.status).to eq('created')
expect(jobs).to include(expected_job_name)
end
end
RSpec.shared_examples 'merge train pipeline' do
let(:ci_merge_request_event_type) { 'merge_train' }
it "succeeds with expected job" do
expect(pipeline.yaml_errors).to be nil
expect(pipeline.status).to eq('created')
expect(jobs).to include('pre-merge-checks')
expect(jobs).not_to include('upload-frontend-fixtures')
end
end
module CiConfigurationValidationHelper
def sync_local_files_to_project(project, user, branch_name, files:)
actions = []
entries = project.repository.tree(branch_name, recursive: true).entries
entries.map! { |e| e.dir? ? project.repository.tree(branch_name, e.path, recursive: true).entries : e }
current_files = entries.flatten.select(&:file?).map(&:path).uniq
# Delete old
actions.concat (current_files - files).map { |file| { action: :delete, file_path: file } }
# Add new
actions.concat (files - current_files).map { |file|
{ action: :create, file_path: file, content: read_file(file) }
}
# Update changed
(current_files & files).each do |file|
content = read_file(file)
if content != project.repository.blob_data_at(branch_name, file)
actions << { action: :update, file_path: file, content: content }
end
end
if actions.any?
puts "Syncing files to #{project.full_path} #{branch_name} branch"
project.repository.commit_files(user, branch_name: branch_name, message: 'syncing', actions: actions)
else
puts "No file syncing needed"
end
end
def read_file(file, ignore_ci_component: true)
content = File.read(file)
return content unless ignore_ci_component
fake_job = <<~YAML
.ignore:
script: echo ok
YAML
file.end_with?('.yml') && %r{^\s*- component:.*CI_SERVER_}.match?(content) ? fake_job : content
end
end

View File

@ -1,475 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
# ***********************************************************************************************************
# The tests in this file are recommended to run locally to test ci configuration changes
# The tests are slow because pipeline simuation takes time given our current pipeline yaml size
# ***********************************************************************************************************
# TEST COVERAGE
#
# GitLab.com gitlab-org/gitlab-foss Project
# Master Pipeline Triggered by Push
# Scheduled Master Pipeline
#
# GitLab.com gitlab-org/gitlab Master Pipeline
# Default Master Pipeline Triggered by Push
# Scheduled pipeline - Nightly
# Scheduled pipeline - Maintenance
#
# Merge Request Pipeline (and merge train pipeline under the same contexts)
# Unlabeled MR Changing GITALY_SERVER_VERSION
# Unlabeled MR Changing Dangerfile, .gitlab/ci/frontend.gitlab-ci.yml
#
# MR Labeled with pipeline:run-all-rspec Changing app/models/user.rb
# MR Labeled with pipeline:expedite pipeline::expedited Changing app/models/user.rb
# MR Labeled with pipeline::tier-1 Changing app/models/user.rb
# MR Labeled with pipeline::tier-2 and pipeline:mr-approved Changing app/models/user.rb
# MR Labeled with pipeline::tier-3 and pipeline:mr-approved Changing app/models/user.rb
# MR Labeled with pipeline:run-as-if-foss Changing app/models/user.rb
#
# Automated MR
# MR Created from release-tools/update-gitaly Source Branch Changing GITALY_SERVER_VERSION
#
# Stable Branch
# MR Targeting a Stable Branch Changing app/models/user.rb
#
# Fork Project MRs
# MR Created from a Fork Project Master Branch Changing package.json
# MR Created from a Fork Project Feature Branch Changing package.json
# ***********************************************************************************************************
# CONTRIBUTE
#
# If you think we are missing coverage for an important pipeline type, please add them.
#
# For example, ci rule changes could break gitlab-foss pipelines, as seen in
# https://gitlab.com/gitlab-org/quality/engineering-productivity/master-broken-incidents/-/issues/7356
# we then added test cases by simulating pipeline for gitlab-org/gitlab-foss
# See `with gitlab.com gitlab-org gitlab-foss project` context below for details.
# ***********************************************************************************************************
RSpec.describe 'ci jobs dependency', feature_category: :tooling do
include ProjectForksHelper
def sync_local_files_to_project(project, user, branch_name, files:)
actions = []
entries = project.repository.tree(branch_name, recursive: true).entries
entries.map! { |e| e.dir? ? project.repository.tree(branch_name, e.path, recursive: true).entries : e }
current_files = entries.flatten.select(&:file?).map(&:path).uniq
# Delete old
actions.concat (current_files - files).map { |file| { action: :delete, file_path: file } }
# Add new
actions.concat (files - current_files).map { |file|
{ action: :create, file_path: file, content: read_file(file) }
}
# Update changed
(current_files & files).each do |file|
content = read_file(file)
if content != project.repository.blob_data_at(branch_name, file)
actions << { action: :update, file_path: file, content: content }
end
end
if actions.any?
puts "Syncing files to #{project.full_path} #{branch_name} branch"
project.repository.commit_files(user, branch_name: branch_name, message: 'syncing', actions: actions)
else
puts "No file syncing needed"
end
end
def read_file(file, ignore_ci_component: true)
content = File.read(file)
return content unless ignore_ci_component
fake_job = <<~YAML
.ignore:
script: echo ok
YAML
file.end_with?('.yml') && %r{^\s*- component:.*CI_SERVER_}.match?(content) ? fake_job : content
end
let(:ci_server_host) { 'gitlab.com' }
let(:ci_project_namespace) { 'gitlab-org' }
let(:ci_project_path) { 'gitlab-org/gitlab' }
let(:ci_project_name) { 'gitlab' }
let(:ci_pipeline_source) { 'push' }
let(:ci_commit_branch) { master_branch }
let(:variables_attributes_base) do
[
{ key: 'CI_SERVER_HOST', value: ci_server_host },
{ key: 'CI_PROJECT_NAMESPACE', value: ci_project_namespace },
{ key: 'CI_PROJECT_PATH', value: ci_project_path },
{ key: 'CI_PROJECT_NAME', value: ci_project_name },
{ key: 'CI_PIPELINE_SOURCE', value: ci_pipeline_source },
{ key: 'CI_COMMIT_BRANCH', value: ci_commit_branch }
]
end
let_it_be(:group) { create(:group, path: 'gitlab-org') }
let_it_be(:gitlab_org_gitlab_project) { create(:project, :empty_repo, group: group, path: 'gitlab') }
let_it_be(:user) { create(:user) }
let_it_be(:ci_glob) { Dir.glob("{.gitlab-ci.yml,.gitlab/**/*.yml}").freeze }
let_it_be(:master_branch) { 'master' }
let(:create_pipeline_service) { Ci::CreatePipelineService.new(gitlab_org_gitlab_project, user, ref: master_branch) }
let(:jobs) { pipeline.stages.flat_map { |s| s.statuses.map(&:name) } }
around do |example|
with_net_connect_allowed { example.run } # creating pipeline requires network call to fetch templates
end
before_all do
gitlab_org_gitlab_project.add_developer(user)
sync_local_files_to_project(
gitlab_org_gitlab_project,
user,
master_branch,
files: ci_glob
)
end
before do
# delete once we have a migration to permanently increase limit
stub_application_setting(max_yaml_size_bytes: 2.megabytes)
end
shared_examples 'master pipeline' do
let(:content) do
gitlab_org_gitlab_project.repository.blob_at(master_branch, '.gitlab-ci.yml').data
end
subject(:pipeline) do
trigger_source = ci_pipeline_source.to_sym
create_pipeline_service
.execute(trigger_source, dry_run: true, content: content, variables_attributes: variables_attributes)
.payload
end
it 'is valid' do
expect(pipeline.yaml_errors).to be nil
expect(pipeline.status).to eq('created')
expect(jobs).to include(expected_job_name)
end
end
context 'with gitlab.com gitlab-org/gitlab master pipeline' do
context 'with default master pipeline' do
let(:variables_attributes) { variables_attributes_base }
let(:expected_job_name) { 'db:migrate:multi-version-upgrade' }
# Test: remove rules from .rails:rules:setup-test-env
it_behaves_like 'master pipeline'
end
context 'with scheduled nightly' do
let(:ci_pipeline_source) { 'schedule' }
let(:expected_job_name) { 'db:rollback single-db' }
let(:variables_attributes) do
[
*variables_attributes_base,
{ key: 'SCHEDULE_TYPE', value: 'nightly' }
]
end
# .if-default-branch-schedule-nightly
# included in .qa:rules:package-and-test-ce
# used by e2e:package-and-test-ce
# needs e2e-test-pipeline-generate
# has rule .qa:rules:determine-e2e-tests
# Test: I can remove this rule from .qa:rules:determine-e2e-tests
# - <<: *if-dot-com-gitlab-org-schedule
# allow_failure: true
it_behaves_like 'master pipeline'
end
context 'with scheduled maintenance' do
let(:ci_pipeline_source) { 'schedule' }
let(:expected_job_name) { 'generate-frontend-fixtures-mapping' }
let(:variables_attributes) do
[
*variables_attributes_base,
{ key: 'SCHEDULE_TYPE', value: 'maintenance' }
]
end
it_behaves_like 'master pipeline'
end
end
context 'with gitlab.com gitlab-org gitlab-foss project' do
let(:ci_project_name) { 'gitlab-foss' }
let(:ci_project_path) { 'gitlab-org/gitlab-foss' }
let(:variables_attributes) { variables_attributes_base }
context 'with master pipeline triggered by push' do
let(:expected_job_name) { 'db:backup_and_restore single-db' }
it_behaves_like 'master pipeline'
end
context 'with scheduled master pipeline' do
let(:ci_pipeline_source) { 'schedule' }
let(:expected_job_name) { 'db:backup_and_restore single-db' }
# Verify by removing the following rule from .qa:rules:e2e:test-on-cng
# - !reference [".qa:rules:package-and-test-never-run", rules]
it_behaves_like 'master pipeline'
end
end
context 'with MR pipeline' do
let(:ci_pipeline_source) { 'merge_request_event' }
let(:ci_merge_request_event_type) { 'merged_result' }
let(:ci_commit_branch) { target_branch } # to simulate merged results pipeline
let(:ci_merge_request_labels_string) { '' }
let(:mr_pipeline_variables_attributes) do
[
*variables_attributes_base,
{ key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: ci_merge_request_event_type },
{ key: 'CI_MERGE_REQUEST_LABELS', value: ci_merge_request_labels_string }
]
end
let(:create_pipeline_service) { Ci::CreatePipelineService.new(target_project, user, ref: target_branch) }
let(:source_project) { gitlab_org_gitlab_project }
let(:target_project) { gitlab_org_gitlab_project }
let(:source_branch) { "feature_branch_ci_#{SecureRandom.uuid}" }
let(:target_branch) { master_branch }
let(:merge_request) do
create(:merge_request,
source_project: source_project,
source_branch: source_branch,
target_project: target_project,
target_branch: target_branch
)
end
subject(:pipeline) do
create_pipeline_service
.execute(
:push,
dry_run: true,
merge_request: merge_request,
variables_attributes: mr_pipeline_variables_attributes
).payload
end
before do
file_change_actions = changed_files.map do |file_path|
action = source_project.repository.blob_at(source_branch, file_path).nil? ? :create : :update
{
action: action,
file_path: file_path,
content: 'content'
}
end
source_project.repository.commit_files(
user,
branch_name: source_branch,
message: 'changes files',
actions: file_change_actions
)
end
after do
source_project.repository.delete_branch(source_branch)
end
shared_examples 'merge request pipeline' do
it "succeeds with expected job" do
expect(pipeline.yaml_errors).to be nil
expect(pipeline.status).to eq('created')
# to confirm that the dependent job is actually created and rule out false positives
expect(jobs).to include(expected_job_name)
end
end
shared_examples 'merge train pipeline' do
let(:ci_merge_request_event_type) { 'merge_train' }
it "succeeds with expected job" do
expect(pipeline.yaml_errors).to be nil
expect(pipeline.status).to eq('created')
expect(jobs).to include('pre-merge-checks')
expect(jobs).not_to include('upload-frontend-fixtures')
end
end
# gitaly, db, backend patterns
context "when unlabeled MR is changing GITALY_SERVER_VERSION" do
let(:changed_files) { ['GITALY_SERVER_VERSION'] }
let(:expected_job_name) { 'eslint' }
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
# Test: remove the following rules from `.frontend:rules:default-frontend-jobs`:
# - <<: *if-default-refs
# changes: *code-backstage-patterns
context 'when unlabled MR is changing Dangerfile, .gitlab/ci/frontend.gitlab-ci.yml' do
let(:changed_files) { ['Dangerfile', '.gitlab/ci/frontend.gitlab-ci.yml'] }
let(:expected_job_name) { 'rspec-all frontend_fixture 1/7' }
it_behaves_like 'merge request pipeline'
end
# Test: remove the following rules from `.frontend:rules:default-frontend-jobs`:
# - <<: *if-merge-request-labels-run-all-rspec
context 'when MR labeled with `pipeline:run-all-rspec` is changing app/models/user.rb' do
let(:ci_merge_request_labels_string) { 'pipeline:run-all-rspec' }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'rspec-all frontend_fixture 1/7' }
it_behaves_like 'merge request pipeline'
end
# code-patterns, code-backstage-patterns, backend patterns, code-qa-patterns
context 'when MR labeled with `pipeline:expedite pipeline::expedited` is changing app/models/user.rb' do
let(:ci_merge_request_labels_string) { 'pipeline:expedite pipeline::expedited' }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'setup-test-env' }
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
context 'when MR labeled with `pipeline::tier-1`' do
let(:ci_merge_request_labels_string) { 'pipeline::tier-1' }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'pipeline-tier-1' }
it_behaves_like 'merge request pipeline'
end
context 'when MR labeled with `pipeline::tier-2` and `pipeline:mr-approved`' do
let(:ci_merge_request_labels_string) { 'pipeline::tier-2 pipeline:mr-approved' }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'pipeline-tier-2' }
it_behaves_like 'merge request pipeline'
end
context 'when MR labeled with `pipeline::tier-3` and `pipeline:mr-approved`' do
let(:ci_merge_request_labels_string) { 'pipeline::tier-3 pipeline:mr-approved' }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'pipeline-tier-3' }
it_behaves_like 'merge request pipeline'
end
context 'when MR labeled with `pipeline:run-as-if-foss` is changing app/models/user.rb' do
let(:ci_merge_request_labels_string) { 'pipeline:run-as-if-foss' }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'start-as-if-foss' }
let(:mr_pipeline_variables_attributes) do
super() << { key: 'AS_IF_FOSS_TOKEN', value: 'foss token' }
end
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
context 'when MR is created from "release-tools/update-gitaly" source branch' do
let(:source_branch) { 'release-tools/update-gitaly' }
let(:changed_files) { ['GITALY_SERVER_VERSION'] }
let(:expected_job_name) { 'update-gitaly-binaries-cache' }
it_behaves_like 'merge request pipeline'
end
# Reminder, we are NOT verifying the CI config from the remote stable branch
# This test just mocks the target branch name to be a stable branch
# the tested config is what's currently in the local .gitlab/ci folders
# Test: remove the following rules from `.frontend:rules:default-frontend-jobs`:
# - <<: *if-default-refs
# changes: *code-backstage-patterns
context 'when MR targeting a stable branch is changing app/models/user.rb' do
let(:target_branch) { '16-10-stable-ee' }
let(:changed_files) { ['app/models/user.rb'] }
let(:expected_job_name) { 'rspec-all frontend_fixture 1/7' }
before do
sync_local_files_to_project(
target_project,
user,
target_branch,
files: ci_glob
)
end
after do
target_project.repository.delete_branch(target_branch)
end
it_behaves_like 'merge request pipeline'
it_behaves_like 'merge train pipeline'
end
context 'with fork project MRs' do
let_it_be(:fork_project_mr_source) { fork_project(gitlab_org_gitlab_project, user, repository: true) }
let(:source_project) { fork_project_mr_source }
let(:target_project) { gitlab_org_gitlab_project }
context 'when MR is created from a fork project master branch' do
let(:source_branch) { master_branch }
let(:target_branch) { master_branch }
let(:changed_files) { ['package.json'] }
let(:expected_job_name) { 'rspec-all frontend_fixture 1/7' }
context 'when running MR pipeline in the context of the fork project' do
let(:ci_project_namespace) { fork_project_mr_source.namespace.full_path }
let(:ci_project_path) { fork_project_mr_source.full_path }
let(:ci_project_name) { fork_project_mr_source.name }
it_behaves_like 'merge request pipeline'
end
context 'when running MR pipeline in the context of canonical project' do
it_behaves_like 'merge request pipeline'
end
it_behaves_like 'merge train pipeline'
end
context 'when MR is created from a fork project feature branch' do
let(:source_branch) { "feature_branch_ci_#{SecureRandom.uuid}" }
let(:target_branch) { master_branch }
let(:changed_files) { ['package.json'] }
let(:expected_job_name) { 'rspec-all frontend_fixture 1/7' }
context 'when running MR pipeline in the context of the fork project' do
let(:ci_project_namespace) { fork_project_mr_source.namespace.full_path }
let(:ci_project_path) { fork_project_mr_source.full_path }
let(:ci_project_name) { fork_project_mr_source.name }
it_behaves_like 'merge request pipeline'
end
context 'when running MR pipeline in the context of canonical project' do
it_behaves_like 'merge request pipeline'
end
it_behaves_like 'merge train pipeline'
end
end
end
end

View File

@ -642,6 +642,20 @@ describe('IssuableItem', () => {
expect(wrapper.emitted('select-issuable')).toEqual([[{ iid, webUrl }]]);
});
it('includes fullPath in emitted event for work items', () => {
const { iid, webUrl } = mockIssuable;
const fullPath = 'gitlab-org/gitlab';
wrapper = createComponent({
preventRedirect: true,
issuable: { ...mockIssuable, namespace: { fullPath } },
});
findIssuableTitleLink().vm.$emit('click', new MouseEvent('click'));
expect(wrapper.emitted('select-issuable')).toEqual([[{ iid, webUrl, fullPath }]]);
});
it('does not apply highlighted class when item is not active', () => {
wrapper = createComponent({
preventRedirect: true,

View File

@ -36,6 +36,9 @@ describe('WorkItemDrawer', () => {
listeners: {
customEvent: mockListener,
},
provide: {
fullPath: '/gitlab-org',
},
stubs: { workItemDetail: true },
apolloProvider: createMockApollo([[deleteWorkItemMutation, deleteWorkItemMutationHandler]]),
});

View File

@ -36,6 +36,8 @@ import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_ro
import WorkItemsListApp from '~/work_items/pages/work_items_list_app.vue';
import { sortOptions, urlSortParams } from '~/work_items/pages/list/constants';
import getWorkItemsQuery from '~/work_items/graphql/list/get_work_items.query.graphql';
import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
import { STATE_CLOSED } from '~/work_items/constants';
import { groupWorkItemsQueryResponse } from '../../mock_data';
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
@ -58,6 +60,7 @@ describeSkipVue3(skipReason, () => {
const findIssuableList = () => wrapper.findComponent(IssuableList);
const findIssueCardStatistics = () => wrapper.findComponent(IssueCardStatistics);
const findIssueCardTimeInfo = () => wrapper.findComponent(IssueCardTimeInfo);
const findDrawer = () => wrapper.findComponent(WorkItemDrawer);
const mountComponent = ({
provide = {},
@ -407,4 +410,94 @@ describeSkipVue3(skipReason, () => {
});
});
});
describe('work item drawer', () => {
describe('when issues_list_drawer feature is disabled', () => {
it('is not rendered when feature is disabled', async () => {
mountComponent({
provide: {
glFeatures: {
issuesListDrawer: false,
},
},
});
await waitForPromises();
expect(findDrawer().exists()).toBe(false);
});
});
describe('when issues_list_drawer feature is enabled', () => {
beforeEach(async () => {
mountComponent({
provide: {
glFeatures: {
issuesListDrawer: true,
},
},
});
await waitForPromises();
});
it('is rendered when feature is enabled', () => {
expect(findDrawer().exists()).toBe(true);
});
describe('selecting issues', () => {
const issue = groupWorkItemsQueryResponse.data.group.workItems.nodes[0];
const payload = {
iid: issue.iid,
webUrl: issue.webUrl,
fullPath: issue.namespace.fullPath,
};
beforeEach(async () => {
findIssuableList().vm.$emit('select-issuable', payload);
await nextTick();
});
it('opens drawer when work item is selected', () => {
expect(findDrawer().props('open')).toBe(true);
expect(findDrawer().props('activeItem')).toEqual(payload);
});
const checkThatDrawerPropsAreEmpty = () => {
expect(findDrawer().props('activeItem')).toBeNull();
expect(findDrawer().props('open')).toBe(false);
};
it('resets the selected item when the drawer is closed', async () => {
findDrawer().vm.$emit('close');
await nextTick();
checkThatDrawerPropsAreEmpty();
});
it('refetches and resets when work item is deleted', async () => {
findDrawer().vm.$emit('workItemDeleted');
await nextTick();
checkThatDrawerPropsAreEmpty();
// first call when mounted, second call after deletion
expect(defaultQueryHandler).toHaveBeenCalledTimes(2);
});
it('refetches when the selected work item is closed', async () => {
// component displays open work items by default
findDrawer().vm.$emit('work-item-updated', {
state: STATE_CLOSED,
});
await nextTick();
// first call when mounted, second call after update
expect(defaultQueryHandler).toHaveBeenCalledTimes(2);
});
});
});
});
});

View File

@ -4,7 +4,9 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['MlModelVersion'], feature_category: :mlops do
let_it_be(:model_version) { create(:ml_model_versions, :with_package, description: 'A description') }
let_it_be(:model_version_markdown) { create(:ml_model_versions, :with_package, description: 'A **description**') }
let_it_be(:project) { model_version.project }
let_it_be(:project_markdown) { model_version_markdown.project }
let_it_be(:current_user) { project.owner }
let(:query) do
@ -17,6 +19,7 @@ RSpec.describe GitlabSchema.types['MlModelVersion'], feature_category: :mlops do
version
packageId
description
descriptionHtml
candidate {
id
}
@ -31,12 +34,43 @@ RSpec.describe GitlabSchema.types['MlModelVersion'], feature_category: :mlops do
)
end
let(:query_markdown) do
%(
query {
mlModel(id: "gid://gitlab/Ml::Model/#{model_version_markdown.model.id}") {
id
latestVersion {
id
version
packageId
description
descriptionHtml
candidate {
id
}
_links {
packagePath
showPath
importPath
}
}
}
}
)
end
let(:data) { GitlabSchema.execute(query, context: { current_user: project.owner }).as_json }
specify { expect(described_class.description).to eq('Version of a machine learning model') }
subject(:data) { GitlabSchema.execute(query, context: { current_user: project.owner }).as_json }
subject(:data_markdown) do
GitlabSchema.execute(query_markdown, context: {
current_user: project_markdown.owner
}).as_json
end
it 'includes all fields' do
expected_fields = %w[id version created_at _links candidate package_id description]
expected_fields = %w[id version created_at _links candidate package_id description description_html]
expect(described_class).to include_graphql_fields(*expected_fields)
end
@ -48,6 +82,7 @@ RSpec.describe GitlabSchema.types['MlModelVersion'], feature_category: :mlops do
'id' => "gid://gitlab/Ml::ModelVersion/#{model_version.id}",
'version' => model_version.version,
'description' => 'A description',
'descriptionHtml' => '<p data-sourcepos="1:1-1:13" dir="auto">A description</p>',
'packageId' => "gid://gitlab/Packages::Package/#{model_version.package_id}",
'candidate' => {
'id' => "gid://gitlab/Ml::Candidate/#{model_version.candidate.id}"
@ -59,4 +94,26 @@ RSpec.describe GitlabSchema.types['MlModelVersion'], feature_category: :mlops do
}
})
end
it 'computes the correct properties with markdown' do
version_data = data_markdown.dig('data', 'mlModel', 'latestVersion')
version = model_version_markdown
expect(version_data).to eq({
'id' => "gid://gitlab/Ml::ModelVersion/#{version.id}",
'version' => version.version,
'description' => 'A **description**',
'descriptionHtml' =>
'<p data-sourcepos="1:1-1:17" dir="auto">A <strong data-sourcepos="1:3-1:17">description</strong></p>',
'packageId' => "gid://gitlab/Packages::Package/#{version.package_id}",
'candidate' => {
'id' => "gid://gitlab/Ml::Candidate/#{version.candidate.id}"
},
'_links' => {
'showPath' =>
"/#{project_markdown.full_path}/-/ml/models/#{version.model.id}/versions/#{version.id}",
'packagePath' => "/#{project_markdown.full_path}/-/packages/#{version.package_id}",
'importPath' => "/api/v4/projects/#{project_markdown.id}/packages/ml_models/#{version.id}/files/"
}
})
end
end

View File

@ -21,7 +21,7 @@ RSpec.describe Projects::Ml::ModelRegistryHelper, feature_category: :mlops do
'canWriteModelRegistry' => true,
'maxAllowedFileSize' => 10737418240,
'mlflowTrackingUrl' => "http://localhost/api/v4/projects/#{project.id}/ml/mlflow/",
'markdownPreviewPath' => "http://localhost/#{project.full_path}/-/ml/preview_markdown"
'markdownPreviewPath' => "/#{project.full_path}/-/preview_markdown"
})
end
@ -58,7 +58,7 @@ RSpec.describe Projects::Ml::ModelRegistryHelper, feature_category: :mlops do
'modelId' => model.id,
'modelName' => 'cool_model',
'latestVersion' => model.latest_version.version,
"markdownPreviewPath" => "http://localhost/#{project.full_path}/-/ml/preview_markdown"
'markdownPreviewPath' => "/#{project.full_path}/-/preview_markdown"
})
end

View File

@ -115,6 +115,7 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
is_signed_in: current_user.present?.to_s,
show_new_issue_link: 'true',
issues_list_path: issues_group_path(group),
report_abuse_path: add_category_abuse_reports_path,
labels_manage_path: group_labels_path(group),
can_admin_label: 'true'
}

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Entities::VirtualRegistries::Packages::Maven::Registry, feature_category: :virtual_registry do
let(:registry) { build_stubbed(:virtual_registries_packages_maven_registry) }
subject { described_class.new(registry).as_json }
it { is_expected.to include(:id, :group_id, :cache_validity_hours, :created_at, :updated_at) }
end

View File

@ -232,10 +232,6 @@ RSpec.describe API::Support::GitAccessActor do
end
describe '#update_last_used_at!' do
before do
stub_feature_flags(disable_ssh_key_used_tracking: false)
end
context 'when initialized with a User' do
let(:user) { build(:user) }
@ -254,14 +250,6 @@ RSpec.describe API::Support::GitAccessActor do
subject.update_last_used_at!
end
it 'does not update `last_used_at` when the functionality is disabled' do
stub_feature_flags(disable_ssh_key_used_tracking: true)
expect(key).not_to receive(:update_last_used_at)
subject.update_last_used_at!
end
end
end
end

View File

@ -81,6 +81,7 @@ issues:
- issuable_resource_links
- synced_epic
- observability_metrics
- observability_logs
work_item_type:
- issues
- namespace

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Middleware::ActionControllerStaticContext, feature_category: :error_budgets do
# to check the end to end tests, go to spec/requests/application_controller_spec.rb
let(:env) do
# env mimicking a controller action parsed by the rails routes
{ 'action_dispatch.request.path_parameters' => { controller: "dashboard/groups", action: "index" } }
end
let(:app) { ->(_env) { [200, {}, ["Hello World"]] } }
context 'when feature flag is disabled' do
it 'does not update context' do
stub_feature_flags(controller_static_context: false)
described_class.new(app).call(env)
expect(Gitlab::ApplicationContext.current.keys).not_to include('meta.feature_category', 'meta.caller_id')
end
end
context 'when feature flag is enabled' do
it 'populates context with static controller attributes' do
stub_feature_flags(controller_static_context: true)
described_class.new(app).call(env)
expect(Labkit::Context.current.to_h).to include({
'meta.feature_category' => 'groups_and_projects',
'meta.caller_id' => 'Dashboard::GroupsController#index'
})
end
end
end

View File

@ -53,6 +53,8 @@ RSpec.describe 'Marginalia spec' do
end
it 'generates a query that includes the component and value' do
stub_feature_flags(controller_static_context: false)
component_map.each do |component, value|
expect(recorded.log.last).to include("#{component}:#{value}")
end
@ -74,6 +76,8 @@ RSpec.describe 'Marginalia spec' do
end
it 'generates a query that includes the component and value' do
stub_feature_flags(controller_static_context: false)
component_map.each do |component, value|
expect(recorded.log.last).to include("#{component}:#{value}")
end

View File

@ -26,4 +26,14 @@ RSpec.describe VirtualRegistries::Packages::Maven::Registry, type: :model, featu
it { is_expected.to validate_presence_of(:group) }
it { is_expected.to validate_numericality_of(:cache_validity_hours).only_integer.is_greater_than_or_equal_to(0) }
end
describe '.for_group' do
let_it_be(:group) { create(:group) }
let_it_be(:registry) { create(:virtual_registries_packages_maven_registry, group: group) }
let_it_be(:other_registry) { create(:virtual_registries_packages_maven_registry) }
subject { described_class.for_group(group) }
it { is_expected.to eq([registry]) }
end
end

View File

@ -43,7 +43,508 @@ RSpec.describe API::VirtualRegistries::Packages::Maven, feature_category: :virtu
end
before do
stub_config(dependency_proxy: { enabled: true })
stub_config(dependency_proxy: { enabled: true }) # not enabled by default
end
describe 'GET /api/v4/virtual_registries/packages/maven/registries' do
let(:group_id) { group.id }
let(:url) { "/virtual_registries/packages/maven/registries?group_id=#{group_id}" }
subject(:api_request) { get api(url), headers: headers }
shared_examples 'successful response' do
it 'returns a successful response' do
api_request
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to contain_exactly(registry.as_json)
end
end
it { is_expected.to have_request_urgency(:low) }
it_behaves_like 'disabled feature flag'
it_behaves_like 'disabled dependency proxy'
it_behaves_like 'not authenticated user'
context 'with valid group_id' do
it_behaves_like 'successful response'
end
context 'with invalid group_id' do
where(:group_id, :status) do
non_existing_record_id | :not_found
'foo' | :bad_request
'' | :bad_request
end
with_them do
it_behaves_like 'returning response status', params[:status]
end
end
context 'with missing group_id' do
let(:url) { '/virtual_registries/packages/maven/registries' }
it 'returns a bad request with missing group_id' do
api_request
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('group_id is missing, group_id is empty')
end
end
context 'with a non member user' do
let_it_be(:user) { build_stubbed(:user) }
where(:group_access_level, :status) do
'PUBLIC' | :ok
'INTERNAL' | :ok
'PRIVATE' | :not_found
end
with_them do
before do
group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_access_level, false))
end
if params[:status] == :ok
it_behaves_like 'successful response'
else
it_behaves_like 'returning response status', params[:status]
end
end
end
context 'for authentication' do
where(:token, :sent_as, :status) do
:personal_access_token | :header | :ok
:personal_access_token | :basic_auth | :ok
:deploy_token | :header | :ok
:deploy_token | :basic_auth | :ok
:job_token | :header | :ok
:job_token | :basic_auth | :ok
end
with_them do
let(:headers) do
case sent_as
when :header
token_header(token)
when :basic_auth
token_basic_auth(token)
end
end
it_behaves_like 'returning response status', params[:status]
end
end
end
describe 'POST /api/v4/virtual_registries/packages/maven/registries' do
let_it_be(:registry_class) { ::VirtualRegistries::Packages::Maven::Registry }
let(:url) { '/virtual_registries/packages/maven/registries' }
subject(:api_request) { post api(url), headers: headers, params: params }
shared_examples 'successful response' do
it 'returns a successful response' do
expect { api_request }.to change { registry_class.count }.by(1)
expect(registry_class.last.group_id).to eq(params[:group_id])
expect(registry_class.last.cache_validity_hours).to eq(
params[:cache_validity_hours] || registry_class.new.cache_validity_hours
)
end
end
context 'with valid params' do
let(:params) { { group_id: group.id, cache_validity_hours: 24 } }
it { is_expected.to have_request_urgency(:low) }
it_behaves_like 'disabled feature flag'
it_behaves_like 'disabled dependency proxy'
it_behaves_like 'not authenticated user'
where(:user_role, :status) do
:owner | :created
:maintainer | :created
:developer | :forbidden
:reporter | :forbidden
:guest | :forbidden
end
with_them do
before do
registry_class.for_group(group).delete_all
group.send(:"add_#{user_role}", user)
end
if params[:status] == :created
it_behaves_like 'successful response'
else
it_behaves_like 'returning response status', params[:status]
end
end
context 'without cache_validity_hours param' do
let(:params) { { group_id: group.id } }
before_all do
registry_class.for_group(group).delete_all
group.add_maintainer(user)
end
it_behaves_like 'successful response'
end
context 'with existing registry' do
before_all do
group.add_maintainer(user)
end
it 'returns a bad request' do
api_request
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({ 'message' => { 'group' => ['has already been taken'] } })
end
end
context 'for authentication' do
before_all do
group.add_maintainer(user)
end
before do
registry_class.for_group(group).delete_all
end
where(:token, :sent_as, :status) do
:personal_access_token | :header | :created
:personal_access_token | :basic_auth | :created
:deploy_token | :header | :forbidden
:deploy_token | :basic_auth | :forbidden
:job_token | :header | :created
:job_token | :basic_auth | :created
end
with_them do
let(:headers) do
case sent_as
when :header
token_header(token)
when :basic_auth
token_basic_auth(token)
end
end
it_behaves_like 'returning response status', params[:status]
end
end
end
context 'with invalid params' do
let(:valid_group_id) { group.id }
before_all do
group.add_maintainer(user)
end
where(:group_id, :cache_validity_hours, :status) do
non_existing_record_id | 1 | :not_found
'foo' | 1 | :bad_request
'' | 1 | :bad_request
ref(:valid_group_id) | 'a' | :bad_request
ref(:valid_group_id) | -1 | :bad_request
end
with_them do
let(:params) { { group_id: group_id, cache_validity_hours: cache_validity_hours } }
it_behaves_like 'returning response status', params[:status]
end
end
context 'with subgroup' do
let(:subgroup) { create(:group, parent: group, visibility_level: group.visibility_level) }
let(:params) { { group_id: subgroup.id, cache_validity_hours: 1 } }
before_all do
group.add_maintainer(user)
end
it 'returns a bad request beacuse it is not a top level group' do
api_request
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({ 'message' => { 'group' => ['must be a top level Group'] } })
end
end
end
describe 'GET /api/v4/virtual_registries/packages/maven/registries/:id' do
let(:registry_id) { registry.id }
let(:url) { "/virtual_registries/packages/maven/registries/#{registry_id}" }
subject(:api_request) { get api(url), headers: headers }
shared_examples 'successful response' do
it 'returns a successful response' do
api_request
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Json.parse(response.body)).to eq(registry.as_json)
end
end
it { is_expected.to have_request_urgency(:low) }
it_behaves_like 'disabled feature flag'
it_behaves_like 'disabled dependency proxy'
it_behaves_like 'not authenticated user'
context 'with valid registry_id' do
it_behaves_like 'successful response'
end
context 'with invalid registry_id' do
where(:registry_id, :status) do
non_existing_record_id | :not_found
'foo' | :bad_request
'' | :bad_request
end
with_them do
it_behaves_like 'returning response status', params[:status]
end
end
context 'with a non member user' do
let_it_be(:user) { build_stubbed(:user) }
where(:group_access_level, :status) do
'PUBLIC' | :ok
'INTERNAL' | :ok
'PRIVATE' | :forbidden
end
with_them do
before do
group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_access_level, false))
end
if params[:status] == :ok
it_behaves_like 'successful response'
else
it_behaves_like 'returning response status', params[:status]
end
end
end
context 'for authentication' do
where(:token, :sent_as, :status) do
:personal_access_token | :header | :ok
:personal_access_token | :basic_auth | :ok
:deploy_token | :header | :ok
:deploy_token | :basic_auth | :ok
:job_token | :header | :ok
:job_token | :basic_auth | :ok
end
with_them do
let(:headers) do
case sent_as
when :header
token_header(token)
when :basic_auth
token_basic_auth(token)
end
end
it_behaves_like 'returning response status', params[:status]
end
end
end
describe 'PATCH /api/v4/virtual_registries/packages/maven/registries/:id' do
let(:registry_id) { registry.id }
let(:url) { "/virtual_registries/packages/maven/registries/#{registry_id}" }
subject(:api_request) { patch api(url), headers: headers, params: params }
shared_examples 'successful response' do
it 'returns a successful response' do
api_request
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['cache_validity_hours']).to eq(registry.reset.cache_validity_hours)
end
end
context 'with valid params' do
let(:params) { { cache_validity_hours: 2 } }
it { is_expected.to have_request_urgency(:low) }
it_behaves_like 'disabled feature flag'
it_behaves_like 'disabled dependency proxy'
it_behaves_like 'not authenticated user'
where(:user_role, :status) do
:owner | :ok
:maintainer | :ok
:developer | :forbidden
:reporter | :forbidden
:guest | :forbidden
end
with_them do
before do
group.send(:"add_#{user_role}", user)
end
if params[:status] == :ok
it_behaves_like 'successful response'
else
it_behaves_like 'returning response status', params[:status]
end
end
context 'for authentication' do
before_all do
group.add_maintainer(user)
end
where(:token, :sent_as, :status) do
:personal_access_token | :header | :ok
:personal_access_token | :basic_auth | :ok
:deploy_token | :header | :forbidden
:deploy_token | :basic_auth | :forbidden
:job_token | :header | :ok
:job_token | :basic_auth | :ok
end
with_them do
let(:headers) do
case sent_as
when :header
token_header(token)
when :basic_auth
token_basic_auth(token)
end
end
it_behaves_like 'returning response status', params[:status]
end
end
end
context 'with invalid params' do
let(:valid_registry_id) { registry.id }
before_all do
group.add_maintainer(user)
end
where(:registry_id, :cache_validity_hours, :status) do
non_existing_record_id | 1 | :not_found
'foo' | 1 | :bad_request
'' | 1 | :not_found
ref(:valid_registry_id) | 'a' | :bad_request
ref(:valid_registry_id) | '' | :bad_request
ref(:valid_registry_id) | -1 | :bad_request
ref(:valid_registry_id) | nil | :bad_request
end
with_them do
let(:params) { { cache_validity_hours: cache_validity_hours } }
it_behaves_like 'returning response status', params[:status]
end
end
end
describe 'DELETE /api/v4/virtual_registries/packages/maven/registries/:id' do
let(:registry_id) { registry.id }
let(:url) { "/virtual_registries/packages/maven/registries/#{registry_id}" }
subject(:api_request) { delete api(url), headers: headers }
shared_examples 'successful response' do
it 'returns a successful response' do
expect { api_request }.to change { ::VirtualRegistries::Packages::Maven::Registry.count }.by(-1)
end
end
it { is_expected.to have_request_urgency(:low) }
it_behaves_like 'disabled feature flag'
it_behaves_like 'disabled dependency proxy'
it_behaves_like 'not authenticated user'
context 'with valid registry_id' do
where(:user_role, :status) do
:owner | :no_content
:maintainer | :no_content
:developer | :forbidden
:reporter | :forbidden
:guest | :forbidden
end
with_them do
before do
group.send(:"add_#{user_role}", user)
end
if params[:status] == :no_content
it_behaves_like 'successful response'
else
it_behaves_like 'returning response status', params[:status]
end
end
end
context 'with invalid registry_id' do
where(:registry_id, :status) do
non_existing_record_id | :not_found
'foo' | :bad_request
'' | :not_found
end
with_them do
it_behaves_like 'returning response status', params[:status]
end
end
context 'for authentication' do
before_all do
group.add_maintainer(user)
end
where(:token, :sent_as, :status) do
:personal_access_token | :header | :no_content
:personal_access_token | :basic_auth | :no_content
:deploy_token | :header | :forbidden
:deploy_token | :basic_auth | :forbidden
:job_token | :header | :no_content
:job_token | :basic_auth | :no_content
end
with_them do
let(:headers) do
case sent_as
when :header
token_header(token)
when :basic_auth
token_basic_auth(token)
end
end
it_behaves_like 'returning response status', params[:status]
end
end
end
describe 'GET /api/v4/virtual_registries/packages/maven/registries/:id/upstreams' do

View File

@ -109,4 +109,113 @@ RSpec.describe ApplicationController, type: :request, feature_category: :shared
}
end
end
describe 'static context middleware', feature_category: :error_budgets do
# to check the middleware tests, go to spec/lib/gitlab/middleware/static_context_middleware_spec.rb
let(:expected_context) do
{ 'meta.feature_category' => 'groups_and_projects',
'meta.caller_id' => 'Dashboard::GroupsController#index' }
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(controller_static_context: false)
end
context 'and action is successfully called' do
it 'pushes static context to current context' do
controller = nil
allow_next_instance_of(Dashboard::GroupsController) do |instance|
controller = instance
end
get '/dashboard/groups' # randomly picked route
expect(response).to have_gitlab_http_status(:found)
expect(controller.instance_variable_get(:@current_context)).to include expected_context
end
end
context 'and an exception is thrown before action' do
it 'does not pushes static context to current context before controller callbacks' do
context = {}
unexpected_error = 'boom 💣💥'
allow_next_instance_of(Dashboard::GroupsController) do |controller|
# picking up a random before_action method to raise an "unexpected" exception
allow(controller).to receive(:authenticate_user!).and_raise(unexpected_error)
context.merge!(Gitlab::ApplicationContext.current.to_h)
end
expect { get '/dashboard/groups' }.to raise_error(unexpected_error)
expect(context.keys).not_to include(['meta.feature_category', 'meta.caller_id'])
end
end
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(controller_static_context: true)
end
context 'when action is successfully called' do
it 'pushes static context to current context' do
context = {}
allow_next_instance_of(Dashboard::GroupsController) do |_controller|
context.merge!(Gitlab::ApplicationContext.current.to_h)
end
get '/dashboard/groups' # randomly picked route
expect(response).to have_gitlab_http_status(:found)
expect(context).to include expected_context
end
end
context 'when an exception is thrown before action' do
it 'pushes static context to current context before controller callbacks' do
context = {}
unexpected_error = 'boom 💣💥'
allow_next_instance_of(Dashboard::GroupsController) do |controller|
# picking up a random before_action method to raise an "unexpected" exception
allow(controller).to receive(:authenticate_user!).and_raise(unexpected_error)
context.merge!(Gitlab::ApplicationContext.current.to_h)
end
expect { get '/dashboard/groups' }.to raise_error(unexpected_error)
expect(context).to include expected_context
end
end
context 'when controller overrides feature_category with nil' do
it 'ignores nil feature category override' do
context = {}
allow_next_instance_of(Projects::NotesController) do |controller|
# mimicking a bug overriding feature_category with nil
allow(controller).to receive(:feature_category).and_return(nil)
context.merge!(Gitlab::ApplicationContext.current.to_h)
end
project = create(:project, :public)
project_snippet = create(:project_snippet, project: project)
create(:note_on_project_snippet, project: project, noteable: project_snippet)
# picking a route targeting a controller that overrides feature_category
get project_noteable_notes_path(
project,
target_type: 'project_snippet',
target_id: project_snippet.id,
html: true
)
expect(context).to include({ 'meta.feature_category' => 'team_planning' })
end
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ConfirmationsController, type: :request, feature_category: :system_access do
describe '#show' do
let_it_be_with_reload(:user) { create(:user, :unconfirmed) }
let(:expected_context) do
{ 'meta.caller_id' => 'ConfirmationsController#show',
'meta.user' => user.username }
end
subject(:perform_request) do
get user_confirmation_path, params: { confirmation_token: user.confirmation_token }
end
include_examples 'set_current_context'
end
end

View File

@ -52,4 +52,57 @@ RSpec.describe OmniauthCallbacksController, feature_category: :system_access do
end
end
end
describe '#atlassian_oauth2' do
describe 'omniauth with strategies for atlassian_oauth2 when the user and identity already exist' do
shared_context 'with sign_up' do
let(:extern_uid) { 'my-uid' }
let(:user) { create(:atlassian_user, extern_uid: extern_uid) }
let(:expected_context) do
{ 'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2',
'meta.user' => user.username }
end
subject do
stub_omniauth_setting(block_auto_created_users: false)
post '/users/auth/atlassian_oauth2/callback'
end
include_examples 'set_current_context'
end
end
end
describe '#saml' do
let(:last_request_id) { 'ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685' }
let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') }
let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
before do
stub_omniauth_saml_config(
enabled: true,
auto_link_saml_user: true,
allow_single_sign_on: ['saml'],
providers: [saml_config]
)
mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response)
end
describe 'with IdP initiated request' do
let(:expected_context) do
{ 'meta.caller_id' => 'OmniauthCallbacksController#saml',
'meta.user' => user.username }
end
subject do
sign_in user
post '/users/auth/saml'
end
include_examples 'set_current_context'
end
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PasswordsController, type: :request, feature_category: :system_access do
describe '#update' do
let(:user) { create(:user, password_automatically_set: true, password_expires_at: 10.minutes.ago) }
let(:expected_context) do
{ 'meta.caller_id' => 'PasswordsController#update',
'meta.user' => user.username }
end
subject(:perform_request) do
password = User.random_password
put user_password_path, params: {
user: {
password: password,
password_confirmation: password,
reset_password_token: user.send_reset_password_instructions
}
}
end
include_examples 'set_current_context'
end
end

View File

@ -3,12 +3,16 @@
require 'spec_helper'
RSpec.describe RegistrationsController, type: :request, feature_category: :system_access do
describe 'POST #create' do
describe '#create' do
let_it_be(:user_attrs) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) }
let(:expected_context) do
{ 'meta.caller_id' => 'RegistrationsController#create' }
end
subject(:request) { post user_registration_path, params: { user: user_attrs } }
it_behaves_like 'Base action controller'
it_behaves_like 'set_current_context'
context 'when email confirmation is required' do
before do
@ -29,4 +33,19 @@ RSpec.describe RegistrationsController, type: :request, feature_category: :syste
end
end
end
describe '#destroy' do
let(:user) { create(:user) }
let(:expected_context) do
{ 'meta.caller_id' => 'RegistrationsController#destroy',
'meta.user' => user.username }
end
subject do
sign_in(user)
delete user_registration_path
end
include_examples 'set_current_context'
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe SessionsController, type: :request, feature_category: :system_access do
describe '#destroy' do
let_it_be(:user) { create(:user) }
let(:expected_context) do
{ 'meta.caller_id' => 'SessionsController#destroy',
'meta.user' => user.username }
end
subject(:perform_request) do
sign_in(user)
post destroy_user_session_path
end
include_examples 'set_current_context'
end
describe '#new' do
let(:expected_context) do
{ 'meta.caller_id' => 'SessionsController#new' }
end
subject(:perform_request) do
get new_user_session_path
end
include_examples 'set_current_context'
end
describe '#create' do
let_it_be(:user) { create(:user) }
let(:expected_context) do
{ 'meta.caller_id' => 'SessionsController#create',
'meta.user' => user.username }
end
subject(:perform_request) do
user.update!(failed_attempts: User.maximum_attempts.pred)
post user_session_path, params: { user: { login: user.username, password: user.password.succ } }
end
include_examples 'set_current_context'
end
end

View File

@ -151,7 +151,7 @@ RSpec.configure do |config|
metadata[:type] = :feature
end
config.define_derived_metadata(file_path: %r{spec/dot_gitlab_ci/job_dependency_spec.rb}) do |metadata|
config.define_derived_metadata(file_path: %r{spec/dot_gitlab_ci/ci_configuration_validation/}) do |metadata|
metadata[:ci_config_validation] = true
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
RSpec.shared_examples "set_current_context" do
it 'sets the metadata of the request in the context' do |example|
raise('this shared example should be used in a request spec only') unless example.metadata[:type] == :request
raise('expected_context should be provided') unless example.metadata[:expected_context].nil?
raise('described_class is invalid') if described_class.nil?
meta = example.metadata[:example_group]
loop do
most_outer_scope = meta[:scoped_id].split(':').size == 1
raise('this shared example should be used within the scope of the controller action') if most_outer_scope
describe_action_scope = meta[:scoped_id].split(':').size == 2
break if describe_action_scope
meta = meta[:parent_example_group]
end
inferred_controller_action = meta[:description]
unless inferred_controller_action.start_with?('#')
raise('controller action describe should be in the form of "#action_name"')
end
inferred_controller_action = inferred_controller_action.delete('#')
expect_next_instance_of(described_class) do |controller|
expect(controller).to receive(inferred_controller_action).and_wrap_original do |m, *args|
m.call(*args)
expect(Gitlab::ApplicationContext.current).to include(expected_context)
end
end
raise('subject/let binding (perform_request) should be provided') unless try(:perform_request) || subject
end
end

View File

@ -31,39 +31,21 @@ RSpec.describe 'user_settings/ssh_keys/_key.html.haml', feature_category: :syste
expect(rendered).to have_button('Remove')
end
context 'when disable_ssh_key_used_tracking is enabled' do
before do
stub_feature_flags(disable_ssh_key_used_tracking: true)
end
it 'displays the correct last used date' do
render
it 'renders "Unavailable" for last used' do
render
expect(rendered).to have_text('Unavailable')
end
expect(rendered).to have_text(l(key.last_used_at, format: "%b %d, %Y"))
end
context 'when disable_ssh_key_used_tracking is disabled' do
before do
stub_feature_flags(disable_ssh_key_used_tracking: false)
context 'when the key has not been used' do
let_it_be(:key) do
build_stubbed(:personal_key, user: user, last_used_at: nil)
end
it 'displays the correct last used date' do
it 'renders "Never" for last used' do
render
expect(rendered).to have_text(l(key.last_used_at, format: "%b %d, %Y"))
end
context 'when the key has not been used' do
let_it_be(:key) do
build_stubbed(:personal_key, user: user, last_used_at: nil)
end
it 'renders "Never" for last used' do
render
expect(rendered).to have_text('Never')
end
expect(rendered).to have_text('Never')
end
end