Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-05-15 15:07:43 +00:00
parent e6fed37d94
commit f8a5275c45
67 changed files with 821 additions and 416 deletions

View File

@ -1463,14 +1463,15 @@
.qa:rules:package-and-test-sidebar: .qa:rules:package-and-test-sidebar:
rules: rules:
- !reference [".qa:rules:package-and-test-common", rules] - <<: *if-merge-request
- <<: *if-dot-com-gitlab-org-schedule changes: *code-patterns
when: manual
allow_failure: true
- <<: *if-default-branch-schedule-nightly
allow_failure: true allow_failure: true
variables: variables:
SKIP_REPORT_IN_ISSUES: "true" SKIP_REPORT_IN_ISSUES: "false"
PROCESS_TEST_RESULTS: "false" PROCESS_TEST_RESULTS: "true"
KNAPSACK_GENERATE_REPORT: "false"
UPDATE_QA_CACHE: "false"
QA_SAVE_TEST_METRICS: "true" QA_SAVE_TEST_METRICS: "true"
QA_EXPORT_TEST_METRICS: "false" QA_EXPORT_TEST_METRICS: "false"

View File

@ -46,7 +46,7 @@ gem 'devise', '~> 4.8.1'
gem 'devise-pbkdf2-encryptable', '~> 0.0.0', path: 'vendor/gems/devise-pbkdf2-encryptable' gem 'devise-pbkdf2-encryptable', '~> 0.0.0', path: 'vendor/gems/devise-pbkdf2-encryptable'
gem 'bcrypt', '~> 3.1', '>= 3.1.14' gem 'bcrypt', '~> 3.1', '>= 3.1.14'
gem 'doorkeeper', '~> 5.6', '>= 5.6.6' gem 'doorkeeper', '~> 5.6', '>= 5.6.6'
gem 'doorkeeper-openid_connect', '~> 1.8', '>= 1.8.5' gem 'doorkeeper-openid_connect', '~> 1.8', '>= 1.8.6'
gem 'rexml', '~> 3.2.5' gem 'rexml', '~> 3.2.5'
gem 'ruby-saml', '~> 1.13.0' gem 'ruby-saml', '~> 1.13.0'
gem 'omniauth', '~> 2.1.0' gem 'omniauth', '~> 2.1.0'

View File

@ -123,7 +123,7 @@
{"name":"docile","version":"1.4.0","platform":"ruby","checksum":"5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3"}, {"name":"docile","version":"1.4.0","platform":"ruby","checksum":"5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3"},
{"name":"domain_name","version":"0.5.20190701","platform":"ruby","checksum":"000a600454cb4a344769b2f10b531765ea7bd3a304fe47ed12e5ca1eab969851"}, {"name":"domain_name","version":"0.5.20190701","platform":"ruby","checksum":"000a600454cb4a344769b2f10b531765ea7bd3a304fe47ed12e5ca1eab969851"},
{"name":"doorkeeper","version":"5.6.6","platform":"ruby","checksum":"2344e86c77770526efcda893b5217aa13d1c7eb1b40de840b58b19eb1ff757e0"}, {"name":"doorkeeper","version":"5.6.6","platform":"ruby","checksum":"2344e86c77770526efcda893b5217aa13d1c7eb1b40de840b58b19eb1ff757e0"},
{"name":"doorkeeper-openid_connect","version":"1.8.5","platform":"ruby","checksum":"d4ee57687945402843c948cee399c758cdddf04468c42b1fb02a8800dd0627f6"}, {"name":"doorkeeper-openid_connect","version":"1.8.6","platform":"ruby","checksum":"8dc46543e697476f441496a5d465bbc68c10d052e54348cec4db06d123b1e003"},
{"name":"dotenv","version":"2.7.6","platform":"ruby","checksum":"2451ed5e8e43776d7a787e51d6f8903b98e446146c7ad143d5678cc2c409d547"}, {"name":"dotenv","version":"2.7.6","platform":"ruby","checksum":"2451ed5e8e43776d7a787e51d6f8903b98e446146c7ad143d5678cc2c409d547"},
{"name":"dry-configurable","version":"0.12.0","platform":"ruby","checksum":"87a9579a04dfbae73e401d694282800d64bbdb8631cb3e987bfb79b673df7c67"}, {"name":"dry-configurable","version":"0.12.0","platform":"ruby","checksum":"87a9579a04dfbae73e401d694282800d64bbdb8631cb3e987bfb79b673df7c67"},
{"name":"dry-container","version":"0.7.2","platform":"ruby","checksum":"a071824ba3451048b23500210f96a2b9facd6e46ac687f65e49c75d18786f6da"}, {"name":"dry-container","version":"0.7.2","platform":"ruby","checksum":"a071824ba3451048b23500210f96a2b9facd6e46ac687f65e49c75d18786f6da"},

View File

@ -393,7 +393,7 @@ GEM
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (5.6.6) doorkeeper (5.6.6)
railties (>= 5) railties (>= 5)
doorkeeper-openid_connect (1.8.5) doorkeeper-openid_connect (1.8.6)
doorkeeper (>= 5.5, < 5.7) doorkeeper (>= 5.5, < 5.7)
jwt (>= 2.5) jwt (>= 2.5)
dotenv (2.7.6) dotenv (2.7.6)
@ -1714,7 +1714,7 @@ DEPENDENCIES
diffy (~> 3.4) diffy (~> 3.4)
discordrb-webhooks (~> 3.4) discordrb-webhooks (~> 3.4)
doorkeeper (~> 5.6, >= 5.6.6) doorkeeper (~> 5.6, >= 5.6.6)
doorkeeper-openid_connect (~> 1.8, >= 1.8.5) doorkeeper-openid_connect (~> 1.8, >= 1.8.6)
duo_api (~> 1.3) duo_api (~> 1.3)
ed25519 (~> 1.3.0) ed25519 (~> 1.3.0)
elasticsearch-api (= 7.13.3) elasticsearch-api (= 7.13.3)

View File

@ -0,0 +1,31 @@
<script>
export default {
inject: {
canAdminAchievement: {
type: Boolean,
required: true,
},
canAwardAchievement: {
type: Boolean,
required: true,
},
groupFullPath: {
type: String,
required: true,
},
groupId: {
type: Number,
required: true,
},
textQuery: {
type: String,
required: false,
default: null,
},
},
};
</script>
<template>
<div></div>
</template>

View File

@ -0,0 +1,7 @@
export const INDEX_ROUTE_NAME = 'index';
export const NEW_ROUTE_NAME = 'new';
export const EDIT_ROUTE_NAME = 'edit';
export const trackViewsOptions = {
category: 'Achievements' /* eslint-disable-line @gitlab/require-i18n-strings */,
action: 'view_achievements_list',
};

View File

@ -0,0 +1,16 @@
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from './constants';
export default [
{
name: INDEX_ROUTE_NAME,
path: '/',
},
{
name: NEW_ROUTE_NAME,
path: '/new',
},
{
name: EDIT_ROUTE_NAME,
path: '/:id/edit',
},
];

View File

@ -0,0 +1,43 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import AchievementsApp from '~/achievements/components/achievements_app.vue';
import routes from '~/achievements/routes';
Vue.use(VueApollo);
Vue.use(VueRouter);
const init = () => {
const el = document.getElementById('js-achievements-app');
if (!el) {
return false;
}
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const { basePath, viewModel } = el.dataset;
const provide = JSON.parse(viewModel);
const router = new VueRouter({
base: basePath,
mode: 'history',
routes,
});
return new Vue({
el,
router,
apolloProvider,
provide: convertObjectPropsToCamelCase(provide),
render(createElement) {
return createElement(AchievementsApp);
},
});
};
init();

View File

@ -30,11 +30,6 @@ export default {
required: false, required: false,
default: 'div', default: 'div',
}, },
collectionStyle: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
@ -94,13 +89,7 @@ export default {
</slot> </slot>
</span> </span>
<span <span class="gl-pr-3 gl-text-gray-900 gl-truncate-end">
class="gl-pr-3 gl-truncate-end gl-text-gray-900"
:class="{
'gl-font-sm gl-font-weight-semibold': collectionStyle,
}"
data-testid="section-title"
>
{{ item.title }} {{ item.title }}
</span> </span>

View File

@ -70,7 +70,6 @@ export default {
:item="sectionItem" :item="sectionItem"
:expanded="expanded" :expanded="expanded"
:separated="true" :separated="true"
collection-style
@collapse-toggle="expanded = !expanded" @collapse-toggle="expanded = !expanded"
> >
<draggable <draggable

View File

@ -150,7 +150,6 @@ export default {
v-for="item in nonStaticItems" v-for="item in nonStaticItems"
:key="item.id" :key="item.id"
:item="item" :item="item"
:collection-style="supportsPins"
tag="li" tag="li"
@pin-add="createPin" @pin-add="createPin"
@pin-remove="destroyPin" @pin-remove="destroyPin"

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Groups
class AchievementsController < Groups::ApplicationController
feature_category :user_profile
urgency :low
before_action :authorize_read_achievement!
private
def authorize_read_achievement!
render_404 unless can?(current_user, :read_achievement, group)
end
end
end

View File

@ -613,8 +613,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
Feature.enabled?(:summarize_my_code_review, current_user) && Feature.enabled?(:summarize_my_code_review, current_user) &&
namespace.group_namespace? && namespace.group_namespace? &&
namespace.licensed_feature_available?(:summarize_my_mr_code_review) && namespace.licensed_feature_available?(:summarize_my_mr_code_review) &&
namespace.experiment_features_enabled && Gitlab::Llm::StageCheck.available?(namespace, :summarize_my_mr_code_review) &&
namespace.third_party_ai_features_enabled &&
merge_request.send_to_ai? merge_request.send_to_ai?
end end
end end

View File

@ -0,0 +1,14 @@
- breadcrumb_title _('Achievements')
- page_title _('Achievements')
- @content_wrapper_class = "gl-relative"
= content_for :after_content do
#js-achievements-form-portal
#js-achievements-app{ data: { base_path: group_achievements_path(@group), view_model: Gitlab::Json.generate({
can_admin_achievement: can?(current_user, :admin_achievement, @group),
can_award_achievement: can?(current_user, :award_achievement, @group),
group_full_path: @group.full_path,
group_id: @group.id,
text_query: params[:search]
}) } }

View File

@ -1272,6 +1272,15 @@
:weight: 1 :weight: 1
:idempotent: false :idempotent: false
:tags: [] :tags: []
- :name: github_importer:github_import_pull_requests_import_merged_by
:worker_name: Gitlab::GithubImport::PullRequests::ImportMergedByWorker
:feature_category: :importers
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :cpu
:weight: 1
:idempotent: false
:tags: []
- :name: github_importer:github_import_pull_requests_import_review - :name: github_importer:github_import_pull_requests_import_review
:worker_name: Gitlab::GithubImport::PullRequests::ImportReviewWorker :worker_name: Gitlab::GithubImport::PullRequests::ImportReviewWorker
:feature_category: :importers :feature_category: :importers

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# TODO: remove in 16.1 milestone
# https://gitlab.com/gitlab-org/gitlab/-/issues/409706
module Gitlab module Gitlab
module GithubImport module GithubImport
class ImportPullRequestMergedByWorker # rubocop:disable Scalability/IdempotentWorker class ImportPullRequestMergedByWorker # rubocop:disable Scalability/IdempotentWorker
@ -12,7 +14,7 @@ module Gitlab
end end
def importer_class def importer_class
Importer::PullRequestMergedByImporter Importer::PullRequests::MergedByImporter
end end
def object_type def object_type

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# TODO: remove in 16.X milestone # TODO: remove in 16.1 milestone
# https://gitlab.com/gitlab-org/gitlab/-/issues/377059 # https://gitlab.com/gitlab-org/gitlab/-/issues/409706
module Gitlab module Gitlab
module GithubImport module GithubImport
class ImportReleaseAttachmentsWorker # rubocop:disable Scalability/IdempotentWorker class ImportReleaseAttachmentsWorker # rubocop:disable Scalability/IdempotentWorker

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module PullRequests
class ImportMergedByWorker # rubocop:disable Scalability/IdempotentWorker
include ObjectImporter
worker_resource_boundary :cpu
def representation_class
Gitlab::GithubImport::Representation::PullRequest
end
def importer_class
Importer::PullRequests::MergedByImporter
end
def object_type
:pull_request_merged_by
end
end
end
end
end

View File

@ -15,7 +15,7 @@ module Gitlab
# client - An instance of Gitlab::GithubImport::Client. # client - An instance of Gitlab::GithubImport::Client.
# project - An instance of Project. # project - An instance of Project.
def import(client, project) def import(client, project)
waiter = Importer::PullRequestsMergedByImporter waiter = Importer::PullRequests::AllMergedByImporter
.new(project, client) .new(project, client)
.execute .execute

View File

@ -1,8 +0,0 @@
---
name: namespace_storage_limit_bypass_date_check
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86794
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/361785
milestone: '15.0'
type: development
group: group::utilization
default_enabled: false

View File

@ -1,23 +0,0 @@
# frozen_string_literal: true
# This pulls in
# https://github.com/doorkeeper-gem/doorkeeper-openid_connect/pull/194
# to ensure generated `kid` values are RFC 7638-compliant.
require 'doorkeeper/openid_connect'
raise 'This patch is only needed for doorkeeper_openid_connect v1.8.5' if Doorkeeper::OpenidConnect::VERSION != '1.8.5'
module Doorkeeper
module OpenidConnect
def self.signing_key
key =
if %i[HS256 HS384 HS512].include?(signing_algorithm)
configuration.signing_key
else
OpenSSL::PKey.read(configuration.signing_key)
end
::JWT::JWK.new(key, { kid_generator: JWT::JWK::Thumbprint })
end
end
end

View File

@ -158,6 +158,8 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resources :contacts, only: [:index, :new, :edit] resources :contacts, only: [:index, :new, :edit]
resources :organizations, only: [:index, :new, :edit] resources :organizations, only: [:index, :new, :edit]
end end
resources :achievements, only: [:index, :new, :edit]
end end
scope( scope(

View File

@ -2,16 +2,20 @@ const { parse, compile: compilerDomCompile } = require('@vue/compiler-dom');
const COMMENT_NODE_TYPE = 3; const COMMENT_NODE_TYPE = 3;
const getPropIndex = (node, prop) => node.props?.findIndex((p) => p.name === prop) ?? -1; const hasProp = (node, prop) => node.props?.some((p) => p.name === prop);
function modifyKeysInsideTemplateTag(templateNode) { function modifyKeysInsideTemplateTag(templateNode) {
if (!templateNode.tag === 'template' || !hasProp(templateNode, 'for')) {
return;
}
let keyCandidate = null; let keyCandidate = null;
for (const node of templateNode.children) { for (const node of templateNode.children) {
const keyBindingIndex = node.props const keyBindingIndex = node.props
? node.props.findIndex((prop) => prop.arg && prop.arg.content === 'key') ? node.props.findIndex((prop) => prop.arg && prop.arg.content === 'key')
: -1; : -1;
if (keyBindingIndex !== -1 && getPropIndex(node, 'for') === -1) { if (keyBindingIndex !== -1 && !hasProp(node, 'for')) {
if (!keyCandidate) { if (!keyCandidate) {
keyCandidate = node.props[keyBindingIndex]; keyCandidate = node.props[keyBindingIndex];
} }
@ -24,40 +28,97 @@ function modifyKeysInsideTemplateTag(templateNode) {
} }
} }
function getSlotName(node) {
return node?.props?.find((prop) => prop.name === 'slot')?.arg?.content;
}
function filterCommentNodeAndTrailingSpace(node, idx, list) {
if (node.type === COMMENT_NODE_TYPE) {
return false;
}
if (node.content !== ' ') {
return true;
}
if (list[idx - 1]?.type === COMMENT_NODE_TYPE) {
return false;
}
return true;
}
function filterCommentNodes(node) {
const { length: originalLength } = node.children;
// eslint-disable-next-line no-param-reassign
node.children = node.children.filter(filterCommentNodeAndTrailingSpace);
if (node.children.length !== originalLength) {
// trim remaining spaces
while (node.children.at(-1)?.content === ' ') {
node.children.pop();
}
}
}
function dropVOnceForChildrenInsideVIfBecauseOfIssue7725(node) {
// See https://github.com/vuejs/core/issues/7725 for details
if (!hasProp(node, 'if')) {
return;
}
node.children?.forEach((child) => {
if (Array.isArray(child.props)) {
// eslint-disable-next-line no-param-reassign
child.props = child.props.filter((prop) => prop.name !== 'once');
}
});
}
function fixSameSlotsInsideTemplateFailingWhenUsingWhitespacePreserveDueToIssue6063(node) {
// See https://github.com/vuejs/core/issues/6063 for details
// eslint-disable-next-line no-param-reassign
node.children = node.children.filter((child, idx) => {
if (child.content !== ' ') {
// We need to drop only comment nodes
return true;
}
const previousNodeSlotName = getSlotName(node.children[idx - 1]);
const nextNodeSlotName = getSlotName(node.children[idx + 1]);
if (previousNodeSlotName && previousNodeSlotName === nextNodeSlotName) {
// We have a space beween two slot entries with same slot name, we need to drop it
return false;
}
return true;
});
}
module.exports = { module.exports = {
parse, parse,
compile(template, options) { compile(template, options) {
const rootNode = parse(template, options); const rootNode = parse(template, options);
// We do not want to switch to whitespace: collapse mode which is Vue.js 3 default
// It will be too devastating to codebase
// However, without `whitespace: condense` Vue will treat spaces between comments
// and nodes itself as text nodes, resulting in multi-root component
// For multi-root component passing classes / attributes fallthrough will not work
// See https://github.com/vuejs/core/issues/7909 for details
// To fix that we simply drop all component comments only on top-level
rootNode.children = rootNode.children.filter((n) => n.type !== COMMENT_NODE_TYPE);
const pendingNodes = [rootNode]; const pendingNodes = [rootNode];
while (pendingNodes.length) { while (pendingNodes.length) {
const currentNode = pendingNodes.pop(); const currentNode = pendingNodes.pop();
if (getPropIndex(currentNode, 'for') !== -1) { if (Array.isArray(currentNode.children)) {
if (currentNode.tag === 'template') { // This one will be dropped all together with compiler when we drop Vue.js 2 support
// This one will be dropped all together with compiler when we drop Vue.js 2 support modifyKeysInsideTemplateTag(currentNode);
modifyKeysInsideTemplateTag(currentNode);
}
// This one will be dropped when https://github.com/vuejs/core/issues/7725 will be fixed dropVOnceForChildrenInsideVIfBecauseOfIssue7725(currentNode);
const vOncePropIndex = getPropIndex(currentNode, 'once');
if (vOncePropIndex !== -1) { // See https://github.com/vuejs/core/issues/7909 for details
currentNode.props.splice(vOncePropIndex, 1); // However, this issue applies not only to root-level nodes
} // But on any level comments could change slot emptiness detection
// so we simply drop them
filterCommentNodes(currentNode);
fixSameSlotsInsideTemplateFailingWhenUsingWhitespacePreserveDueToIssue6063(currentNode);
currentNode.children.forEach((child) => pendingNodes.push(child));
} }
currentNode.children?.forEach((child) => pendingNodes.push(child));
} }
return compilerDomCompile(rootNode, options); return compilerDomCompile(rootNode, options);

View File

@ -0,0 +1,41 @@
# This is a template for a feature deprecation.
#
# Please refer to the deprecation guidelines to confirm your understanding of the
# definitions for "Deprecation", "End of Support", and "Removal":
# https://docs.gitlab.com/ee/development/deprecation_guidelines/#terminology
#
# Deprecations must be announced at least three releases prior to removal.
# See the OPTIONAL END OF SUPPORT FIELDS section below if an End of Support period also applies.
#
# Breaking changes must happen in a major release.
#
# For more information please refer to the handbook documentation here:
# https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations
#
# Please delete this line and above before submitting your merge request.
#
# REQUIRED FIELDS
#
- title: "`POST ci/lint` API endpoint removed" # (required) The name of the feature to be deprecated
announcement_milestone: "15.7" # (required) The milestone when this feature was first announced as deprecated.
removal_milestone: "16.0" # (required) The milestone when this feature is planned to be removed
breaking_change: true # (required) If this deprecation is a breaking change, set this value to true
reporter: dhershkovitch # (required) GitLab username of the person reporting the deprecation
stage: verify # (required) String value of the stage that the feature was created in. e.g., Growth
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/381669 # (required) Link to the deprecation issue in GitLab
body: | # (required) Do not modify this line, instead modify the lines below.
The `POST ci/lint` API endpoint was deprecated in 15.7, and removed in 16.0. This endpoint did not validate the full range of CI/CD configuration options. Instead, use [`POST /projects/:id/ci/lint`](https://docs.gitlab.com/ee/api/lint.html#validate-a-ci-yaml-configuration-with-a-namespace), which properly validates CI/CD configuration.
#
# OPTIONAL END OF SUPPORT FIELDS
#
# If an End of Support period applies, the announcement should be shared with GitLab Support
# in the `#spt_managers` channel in Slack, and mention `@gitlab-com/support` in this MR.
#
end_of_support_milestone: # (optional) Use "XX.YY" format. The milestone when support for this feature will end.
#
# OTHER OPTIONAL FIELDS
#
tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
documentation_url: # (optional) This is a link to the current documentation page
image_url: # (optional) This is a link to a thumbnail image depicting the feature
video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg

View File

@ -761,7 +761,7 @@ users, [see what to do when no users are found](#no-users-are-found).
### GitLab logs ### GitLab logs
If a user account is blocked or unblocked due to the LDAP configuration, a If a user account is blocked or unblocked due to the LDAP configuration, a
message is [logged to `application.log`](../../logs/index.md#applicationlog). message is [logged to `application_json.log`](../../logs/index.md#application_jsonlog).
If there is an unexpected error during an LDAP lookup (configuration error, If there is an unexpected error during an LDAP lookup (configuration error,
timeout), the sign-in is rejected and a message is [logged to `production.log`](../../logs/index.md#productionlog). timeout), the sign-in is rejected and a message is [logged to `production.log`](../../logs/index.md#productionlog).

View File

@ -350,7 +350,9 @@ the `view_duration_s` is calculated by [`duration_s - db_duration_s`](https://gi
Therefore, `view_duration_s` can be affected by multiple different factors, like read-write Therefore, `view_duration_s` can be affected by multiple different factors, like read-write
process on Redis or external HTTP, not only the serialization process. process on Redis or external HTTP, not only the serialization process.
## `application.log` ## `application.log` (deprecated)
> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111046) in GitLab 15.10.
Depending on your installation method, this file is located at: Depending on your installation method, this file is located at:

View File

@ -29,7 +29,7 @@ You must replace the `vault.example.com` URL below with the URL of your Vault se
## How it works ## How it works
Each job has JSON Web Token (JWT) provided as CI/CD variable named `CI_JOB_JWT`. This JWT can be used to authenticate with Vault using the [JWT Auth](https://developer.hashicorp.com/vault/docs/auth/jwt#jwt-authentication) method. ID tokens are JSON Web Tokens (JWTs) used for OIDC authentication with third-party services. If a job has at least one ID token defined, the `secrets` keyword automatically uses that token to authenticate with Vault.
The following fields are included in the JWT: The following fields are included in the JWT:
@ -256,61 +256,36 @@ $ vault write auth/jwt/config \
For the full list of available configuration options, see Vault's [API documentation](https://developer.hashicorp.com/vault/api-docs/auth/jwt#configure). For the full list of available configuration options, see Vault's [API documentation](https://developer.hashicorp.com/vault/api-docs/auth/jwt#configure).
The following job, when run for the default branch, is able to read secrets under `secret/myproject/staging/`, but not the secrets under `secret/myproject/production/`: In GitLab, create the following [CI/CD variables](../../variables/index.md#for-a-project) to provide details about your Vault server:
- `VAULT_SERVER_URL` - The URL of your Vault server, for example `https://vault.example.com:8200`.
- `VAULT_AUTH_ROLE` - Optional. The role to use when attempting to authenticate. If no role is specified, Vault uses the [default role](https://developer.hashicorp.com/vault/api-docs/auth/jwt#default_role) specified when the authentication method was configured.
- `VAULT_AUTH_PATH` - Optional. The path where the authentication method is mounted. Default is `jwt`.
- `VAULT_NAMESPACE` - Optional. The [Vault Enterprise namespace](https://developer.hashicorp.com/vault/docs/enterprise/namespaces) to use for reading secrets and authentication. If no namespace is specified, Vault uses the root (`/`) namespace. The setting is ignored by Vault Open Source.
The following job, when run for the default branch, can read secrets under `secret/myproject/staging/`, but not the secrets under `secret/myproject/production/`:
```yaml ```yaml
read_secrets: job_with_secrets:
image: vault:latest id_tokens:
VAULT_ID_TOKEN:
aud: https://example.vault.com
secrets:
STAGING_DB_PASSWORD:
vault: secret/myproject/staging/db/password@secrets # authenticates using $VAULT_ID_TOKEN
script: script:
# Check job's ref name - access-staging-db.sh --token $STAGING_DB_PASSWORD
- echo $CI_COMMIT_REF_NAME
# and is this ref protected
- echo $CI_COMMIT_REF_PROTECTED
# Vault's address can be provided here or as CI/CD variable
- export VAULT_ADDR=http://vault.example.com:8200
# Authenticate and get token. Token expiry time and other properties can be configured
# when configuring JWT Auth - https://developer.hashicorp.com/vault/api-docs/auth/jwt#parameters-1
- export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-staging jwt=$CI_JOB_JWT)"
# Now use the VAULT_TOKEN to read the secret and store it in an environment variable
- export PASSWORD="$(vault kv get -field=password secret/myproject/staging/db)"
# Use the secret
- echo $PASSWORD
# This will fail because the role myproject-staging can not read secrets from secret/myproject/production/*
- export PASSWORD="$(vault kv get -field=password secret/myproject/production/db)"
``` ```
NOTE: In this example:
If you're using a Vault instance provided by HashiCorp Cloud Platform,
you need to export the `VAULT_NAMESPACE` variable. Its default value is `admin`.
![read secrets staging example](img/vault-read-secrets-staging.png) - `@secrets` - The vault name, where your Secrets Engines are enabled.
- `secret/myproject/staging/db` - The path location of the secret in Vault.
The following job is able to authenticate using the `myproject-production` role and read secrets under `/secret/myproject/production/`: - `password` The field to be fetched within the referenced secret.
```yaml
read_secrets:
image: vault:latest
script:
# Check job's ref name
- echo $CI_COMMIT_REF_NAME
# and is this ref protected
- echo $CI_COMMIT_REF_PROTECTED
# Vault's address can be provided here or as CI/CD variable
- export VAULT_ADDR=http://vault.example.com:8200
# Authenticate and get token. Token expiry time and other properties can be configured
# when configuring JWT Auth - https://developer.hashicorp.com/vault/api-docs/auth/jwt#parameters-1
- export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-production jwt=$CI_JOB_JWT)"
# Now use the VAULT_TOKEN to read the secret and store it in environment variable
- export PASSWORD="$(vault kv get -field=password secret/myproject/production/db)"
# Use the secret
- echo $PASSWORD
```
![read secrets production example](img/vault-read-secrets-production.png)
### Limit token access to Vault secrets ### Limit token access to Vault secrets
You can control `CI_JOB_JWT` access to Vault secrets by using Vault protections You can control ID token access to Vault secrets by using Vault protections
and GitLab features. For example, restrict the token by: and GitLab features. For example, restrict the token by:
- Using Vault [bound claims](https://developer.hashicorp.com/vault/docs/auth/jwt#bound-claims) - Using Vault [bound claims](https://developer.hashicorp.com/vault/docs/auth/jwt#bound-claims)

View File

@ -65,7 +65,7 @@ test2:
`&` sets up the name of the anchor (`job_configuration`), `<<` means "merge the `&` sets up the name of the anchor (`job_configuration`), `<<` means "merge the
given hash into the current one," and `*` includes the named anchor given hash into the current one," and `*` includes the named anchor
(`job_configuration` again). The expanded version of this example is: (`job_configuration` again). The [expanded](../pipeline_editor/index.md#view-full-configuration) version of this example is:
```yaml ```yaml
.job_template: .job_template:
@ -123,7 +123,7 @@ test:mysql:
services: *mysql_configuration services: *mysql_configuration
``` ```
The expanded version is: The [expanded](../pipeline_editor/index.md#view-full-configuration) version is:
```yaml ```yaml
.job_template: .job_template:

View File

@ -83,3 +83,65 @@ Quoting the [documentation](https://clickhouse.com/docs/en/sql-reference/stateme
> If there's some aggregation in the view query, it's applied only to the batch > If there's some aggregation in the view query, it's applied only to the batch
> of freshly inserted data. Any changes to existing data of the source table > of freshly inserted data. Any changes to existing data of the source table
> (like update, delete, drop a partition, etc.) do not change the materialized view. > (like update, delete, drop a partition, etc.) do not change the materialized view.
## Secure and sensible defaults
ClickHouse instances should follow these security recommendations:
### Users
Files: `users.xml` and `config.xml`.
| Topic | Security Requirement | Reason |
| ----- | -------------------- | ------ |
| [`user_name/password`](https://clickhouse.com/docs/en/operations/settings/settings-users/#user-namepassword) | Usernames **must not** be blank. Passwords **must** use `password_sha256_hex` and **must not** be blank. | `plaintext` and `password_double_sha1_hex` are insecure. If username isn't specified, [`default` is used with no password](https://clickhouse.com/docs/en/operations/settings/settings-users/). |
| [`access_management`](https://clickhouse.com/docs/en/operations/settings/settings-users/#access_management-user-setting) | Use Server [configuration files](https://clickhouse.com/docs/en/operations/configuration-files) `users.xml` and `config.xml`. Avoid SQL-driven workflow. | SQL-driven workflow implies that at least one user has `access_management` which can be avoided via configuration files. These files are easier to audit and monitor too, considering that ["You can't manage the same access entity by both configuration methods simultaneously."](https://clickhouse.com/docs/en/operations/access-rights/#access-control). |
| [`user_name/networks`](https://clickhouse.com/docs/en/operations/settings/settings-users/#user-namenetworks) | At least one of `<ip>`, `<host>`, `<host_regexp>` **must** be set. Do not use `<ip>::/0</ip>` to open access for any network. | Network controls. ([Trust cautiously](https://about.gitlab.com/handbook/security/architecture/#trust-cautiously) principle) |
| [`user_name/profile`](https://clickhouse.com/docs/en/operations/settings/settings-users/#user-nameprofile) | Use profiles to set similar properties across multiple users and set limits (from the user interface). | [Least privilege](https://about.gitlab.com/handbook/security/architecture/#assign-the-least-privilege-possible) principle and limits. |
| [`user_name/quota`](https://clickhouse.com/docs/en/operations/settings/settings-users/#user-namequota) | Set quotas for users whenever possible. | Limit resource usage over a period of time or track the use of resources. |
| [`user_name/databases`](https://clickhouse.com/docs/en/operations/settings/settings-users/#user-namedatabases) | Restrict access to data, and avoid users with full access. | [Least privilege](https://about.gitlab.com/handbook/security/architecture/#assign-the-least-privilege-possible) principle. |
### Network
Files: `config.xml`
| Topic | Security Requirement | Reason |
| ----- | -------------------- | ------ |
| [`mysql_port`](https://clickhouse.com/docs/en/operations/server-configuration-parameters/settings/#server_configuration_parameters-mysql_port) | Disable MySQL access unless strictly necessary:<br/> `<!-- <mysql_port>9004</mysql_port> -->`. | Close unnecessary ports and features exposure. ([Defense in depth](https://about.gitlab.com/handbook/security/architecture/#implement-defense-in-depth) principle) |
| [`postgresql_port`](https://clickhouse.com/docs/en/operations/server-configuration-parameters/settings/#server_configuration_parameters-postgresql_port) | Disable PostgreSQL access unless strictly necessary:<br/> `<!-- <mysql_port>9005</mysql_port> -->` | Close unnecessary ports and features exposure. ([Defense in depth](https://about.gitlab.com/handbook/security/architecture/#implement-defense-in-depth) principle) |
| [`http_port/https_port`](https://clickhouse.com/docs/en/operations/server-configuration-parameters/settings/#http-porthttps-port) & [`tcp_port/tcp_port_secure`](https://clickhouse.com/docs/en/operations/server-configuration-parameters/settings/#http-porthttps-port) | Configure [SSL-TLS](https://clickhouse.com/docs/en/guides/sre/configuring-ssl), and disable non SSL ports:<br/>`<!-- <http_port>8123</http_port> -->`<br/>`<!-- <tcp_port>9000</tcp_port> -->`<br/>and enable secure ports:<br/>`<https_port>8443</https_port>`<br/>`<tcp_port_secure>9440</tcp_port_secure>` | Encrypt data in transit. ([Defense in depth](https://about.gitlab.com/handbook/security/architecture/#implement-defense-in-depth) principle) |
| [`interserver_http_host`](https://clickhouse.com/docs/en/operations/server-configuration-parameters/settings/#interserver-http-host) | Disable `interserver_http_host` in favor of `interserver_https_host` (`<interserver_https_port>9010</interserver_https_port>`) if ClickHouse is configured as a cluster. | Encrypt data in transit. ([Defense in depth](https://about.gitlab.com/handbook/security/architecture/#implement-defense-in-depth) principle) |
### Storage
| Topic | Security Requirement | Reason |
| ----- | -------------------- | ------ |
| Permissions | ClickHouse runs by default with the `clickhouse` user. Running as `root` is never needed. Use the principle of least privileges for the folders: `/etc/clickhouse-server`, `/var/lib/clickhouse`, `/var/log/clickhouse-server`. These folders must belong to the `clickhouse` user and group, and no other system user must have access to them. | Default passwords, ports and rules are "open doors". ([Fail securely & use secure defaults](https://about.gitlab.com/handbook/security/architecture/#fail-securely--use-secure-defaults) principle) |
| Encryption | Use an encrypted storage for logs and data if RED data is processed. On Kubernetes, the [StorageClass](https://kubernetes.io/docs/concepts/storage/storage-classes/) used must be encrypted. | Encrypt data at rest. ([Defense in depth](https://about.gitlab.com/handbook/security/architecture/#implement-defense-in-depth)) |
### Logging
| Topic | Security Requirement | Reason |
| ----- | -------------------- | ------ |
| `logger` | `Log` and `errorlog` **must** be defined and writable by `clickhouse`. | Make sure logs are stored. |
| SIEM | If hosted on GitLab.com, the ClickHouse instance or cluster **must** report [logs to our SIEM](https://internal-handbook.gitlab.io/handbook/security/infrastructure_security_logging/tooling/devo/) (internal link). | [GitLab logs critical information system activity](https://about.gitlab.com/handbook/security/audit-logging-policy.html). |
| Log sensitive data | Query masking rules **must** be used if sensitive data can be logged. See [example masking rules](#example-masking-rules). | [Column level encryption](https://clickhouse.com/docs/en/sql-reference/functions/encryption-functions/) can be used and leak sensitive data (keys) in logs. |
#### Example masking rules
```xml
<query_masking_rules>
<rule>
<name>hide SSN</name>
<regexp>(^|\D)\d{3}-\d{2}-\d{4}($|\D)</regexp>
<replace>000-00-0000</replace>
</rule>
<rule>
<name>hide encrypt/decrypt arguments</name>
<regexp>
((?:aes_)?(?:encrypt|decrypt)(?:_mysql)?)\s*\(\s*(?:'(?:\\'|.)+'|.*?)\s*\)
</regexp>
<replace>\1(???)</replace>
</rule>
</query_masking_rules>
```

View File

@ -81,7 +81,7 @@ This worker imports the pull requests' _merged-by_ user information. The
[_List pull requests_](https://docs.github.com/en/rest/pulls#list-pull-requests) [_List pull requests_](https://docs.github.com/en/rest/pulls#list-pull-requests)
API doesn't provide this information. Therefore, this stage must fetch each merged pull request API doesn't provide this information. Therefore, this stage must fetch each merged pull request
individually to import this information. A individually to import this information. A
`Gitlab::GithubImport::ImportPullRequestMergedByWorker` job is scheduled for each fetched pull `Gitlab::GithubImport::PullRequests::ImportMergedByWorker` job is scheduled for each fetched pull
request. request.
### 7. Stage::ImportPullRequestsReviewRequestsWorker ### 7. Stage::ImportPullRequestsReviewRequestsWorker

View File

@ -1989,20 +1989,6 @@ For more information, refer to [security report validation](https://docs.gitlab.
<div class="deprecation breaking-change" data-milestone="16.0"> <div class="deprecation breaking-change" data-milestone="16.0">
### Self-monitoring project is removed
<div class="deprecation-notes">
- Announced in: GitLab <span class="milestone">14.9</span>
- This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/groups/gitlab-org/-/epics/10030).
</div>
GitLab self-monitoring project was meant to enable self-hosted GitLab administrators to visualize performance metrics of GitLab within GitLab itself. This feature relied on GitLab Metrics dashboards. With metrics dashboard being removed, self-monitoring project is also removed. We recommended that self-hosted users monitor their GitLab instance with alternative visualization tools, such as Grafana.
</div>
<div class="deprecation breaking-change" data-milestone="16.0">
### Shimo integration ### Shimo integration
<div class="deprecation-notes"> <div class="deprecation-notes">

View File

@ -442,6 +442,14 @@ Review the details carefully before upgrading.
Version 14.x.x [security report schemas](https://gitlab.com/gitlab-org/security-products/security-report-schemas) have been removed. Version 14.x.x [security report schemas](https://gitlab.com/gitlab-org/security-products/security-report-schemas) have been removed.
Security reports that use schema version 14.x.x will cause an error in the pipeline's **Security** tab. For more information, refer to [security report validation](https://docs.gitlab.com/ee/user/application_security/#security-report-validation). Security reports that use schema version 14.x.x will cause an error in the pipeline's **Security** tab. For more information, refer to [security report validation](https://docs.gitlab.com/ee/user/application_security/#security-report-validation).
### Self-monitoring project is removed
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
GitLab self-monitoring project was meant to enable self-hosted GitLab administrators to visualize performance metrics of GitLab within GitLab itself. This feature relied on GitLab Metrics dashboards. With metrics dashboard being removed, self-monitoring project is also removed. We recommended that self-hosted users monitor their GitLab instance with alternative visualization tools, such as Grafana.
### Starboard directive in the config for the GitLab agent for Kubernetes removed ### Starboard directive in the config for the GitLab agent for Kubernetes removed
WARNING: WARNING:
@ -539,6 +547,14 @@ The predefined CI/CD variables that start with `CI_BUILD_*` were deprecated in G
| `CI_BUILD_TOKEN` | `CI_JOB_TOKEN` | | `CI_BUILD_TOKEN` | `CI_JOB_TOKEN` |
| `CI_BUILD_TRIGGERED` | `CI_PIPELINE_TRIGGERED` | | `CI_BUILD_TRIGGERED` | `CI_PIPELINE_TRIGGERED` |
### `POST ci/lint` API endpoint removed
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
The `POST ci/lint` API endpoint was deprecated in 15.7, and removed in 16.0. This endpoint did not validate the full range of CI/CD configuration options. Instead, use [`POST /projects/:id/ci/lint`](https://docs.gitlab.com/ee/api/lint.html#validate-a-ci-yaml-configuration-with-a-namespace), which properly validates CI/CD configuration.
### vulnerabilityFindingDismiss GraphQL mutation ### vulnerabilityFindingDismiss GraphQL mutation
WARNING: WARNING:

View File

@ -458,7 +458,7 @@ For multi-node systems we recommend ingesting the logs into services like Elasti
| Log file | Contents | | Log file | Contents |
|:------------------------|:---------| |:------------------------|:---------|
| `application.log` | GitLab user activity | | `application_json.log` | GitLab user activity |
| `git_json.log` | Failed GitLab interaction with Git repositories | | `git_json.log` | Failed GitLab interaction with Git repositories |
| `production.log` | Requests received from Puma, and the actions taken to serve those requests | | `production.log` | Requests received from Puma, and the actions taken to serve those requests |
| `sidekiq.log` | Background jobs | | `sidekiq.log` | Background jobs |

View File

@ -50,6 +50,11 @@ configured for personal access tokens.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/348660) in GitLab 15.3, default expiration of 30 days and default role of Guest is populated in the UI. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/348660) in GitLab 15.3, default expiration of 30 days and default role of Guest is populated in the UI.
> - Ability to create non-expiring group access tokens [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/392855) in GitLab 16.0. > - Ability to create non-expiring group access tokens [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/392855) in GitLab 16.0.
WARNING:
Project access tokens are treated as [internal users](../../../development/internal_users.md).
If an internal user creates a project access token, that token is able to access
all projects that have visibility level set to [Internal](../../public_access.md).
To create a group access token: To create a group access token:
1. On the top bar, select **Main menu > Groups** and find your group. 1. On the top bar, select **Main menu > Groups** and find your group.
@ -66,11 +71,6 @@ To create a group access token:
A group access token is displayed. Save the group access token somewhere safe. After you leave or refresh the page, you can't view it again. A group access token is displayed. Save the group access token somewhere safe. After you leave or refresh the page, you can't view it again.
WARNING:
Group access tokens are treated as [internal users](../../../development/internal_users.md).
If an internal user creates a group access token, that token is able to access all
groups that have visibility level set to [Internal](../../public_access.md).
## Create a group access token using Rails console ## Create a group access token using Rails console
GitLab 14.6 and earlier doesn't support creating group access tokens using the UI GitLab 14.6 and earlier doesn't support creating group access tokens using the UI

View File

@ -50,6 +50,11 @@ configured for personal access tokens.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/348660) in GitLab 15.3, default expiration of 30 days and default role of Guest is populated in the UI. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/348660) in GitLab 15.3, default expiration of 30 days and default role of Guest is populated in the UI.
> - Ability to create non-expiring project access tokens [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/392855) in GitLab 16.0. > - Ability to create non-expiring project access tokens [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/392855) in GitLab 16.0.
WARNING:
Project access tokens are treated as [internal users](../../../development/internal_users.md).
If an internal user creates a project access token, that token is able to access
all projects that have visibility level set to [Internal](../../public_access.md).
To create a project access token: To create a project access token:
1. On the top bar, select **Main menu > Projects** and find your project. 1. On the top bar, select **Main menu > Projects** and find your project.
@ -66,11 +71,6 @@ To create a project access token:
A project access token is displayed. Save the project access token somewhere safe. After you leave or refresh the page, you can't view it again. A project access token is displayed. Save the project access token somewhere safe. After you leave or refresh the page, you can't view it again.
WARNING:
Project access tokens are treated as [internal users](../../../development/internal_users.md).
If an internal user creates a project access token, that token is able to access
all projects that have visibility level set to [Internal](../../public_access.md).
## Revoke a project access token ## Revoke a project access token
To revoke a project access token: To revoke a project access token:

View File

@ -51,6 +51,7 @@ module.exports = (path, options = {}) => {
experimentalCSSCompile: false, experimentalCSSCompile: false,
compiler: require.resolve('./config/vue3migration/compiler'), compiler: require.resolve('./config/vue3migration/compiler'),
compilerOptions: { compilerOptions: {
whitespace: 'preserve',
compatConfig: { compatConfig: {
MODE: 2, MODE: 2,
}, },

View File

@ -1,69 +0,0 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
class PullRequestMergedByImporter
# pull_request - An instance of
# `Gitlab::GithubImport::Representation::PullRequest`
# project - An instance of `Project`
# client - An instance of `Gitlab::GithubImport::Client`
def initialize(pull_request, project, client)
@pull_request = pull_request
@project = project
@client = client
end
def execute
user_finder = GithubImport::UserFinder.new(project, client)
gitlab_user_id = user_finder.user_id_for(pull_request.merged_by)
metrics_upsert(gitlab_user_id)
add_note!
end
private
attr_reader :project, :pull_request, :client
def metrics_upsert(gitlab_user_id)
MergeRequest::Metrics.upsert({
target_project_id: project.id,
merge_request_id: merge_request.id,
merged_by_id: gitlab_user_id,
merged_at: pull_request.merged_at,
created_at: timestamp,
updated_at: timestamp
}, unique_by: :merge_request_id)
end
def add_note!
merge_request.notes.create!(
importing: true,
note: missing_author_note,
author_id: project.creator_id,
project: project,
created_at: pull_request.merged_at
)
end
def merge_request
@merge_request ||= project.merge_requests.find_by_iid(pull_request.iid)
end
def timestamp
@timestamp ||= Time.new.utc
end
def missing_author_note
s_("GitHubImporter|*Merged by: %{author} at %{timestamp}*") % {
author: pull_request.merged_by&.login || 'ghost',
timestamp: pull_request.merged_at
}
end
end
end
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
module PullRequests
class AllMergedByImporter
include ParallelScheduling
def importer_class
MergedByImporter
end
def representation_class
Gitlab::GithubImport::Representation::PullRequest
end
def sidekiq_worker_class
Gitlab::GithubImport::PullRequests::ImportMergedByWorker
end
def collection_method
:pull_requests_merged_by
end
def object_type
:pull_request_merged_by
end
def id_for_already_imported_cache(merge_request)
merge_request.id
end
def each_object_to_import
merge_requests_to_import.find_each do |merge_request|
Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :fetched)
pull_request = client.pull_request(project.import_source, merge_request.iid)
yield(pull_request)
mark_as_imported(merge_request)
end
end
private
# Returns only the merge requests that still have merged_by to be imported.
def merge_requests_to_import
project.merge_requests.id_not_in(already_imported_objects).with_state(:merged)
end
def already_imported_objects
Gitlab::Cache::Import::Caching.values_from_set(already_imported_cache_key)
end
end
end
end
end
end

View File

@ -0,0 +1,71 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
module PullRequests
class MergedByImporter
# pull_request - An instance of
# `Gitlab::GithubImport::Representation::PullRequest`
# project - An instance of `Project`
# client - An instance of `Gitlab::GithubImport::Client`
def initialize(pull_request, project, client)
@pull_request = pull_request
@project = project
@client = client
end
def execute
user_finder = GithubImport::UserFinder.new(project, client)
gitlab_user_id = user_finder.user_id_for(pull_request.merged_by)
metrics_upsert(gitlab_user_id)
add_note!
end
private
attr_reader :project, :pull_request, :client
def metrics_upsert(gitlab_user_id)
MergeRequest::Metrics.upsert({
target_project_id: project.id,
merge_request_id: merge_request.id,
merged_by_id: gitlab_user_id,
merged_at: pull_request.merged_at,
created_at: timestamp,
updated_at: timestamp
}, unique_by: :merge_request_id)
end
def add_note!
merge_request.notes.create!(
importing: true,
note: missing_author_note,
author_id: project.creator_id,
project: project,
created_at: pull_request.merged_at
)
end
def merge_request
@merge_request ||= project.merge_requests.find_by_iid(pull_request.iid)
end
def timestamp
@timestamp ||= Time.new.utc
end
def missing_author_note
format(s_("GitHubImporter|*Merged by: %{author} at %{timestamp}*"),
author: pull_request.merged_by&.login || 'ghost',
timestamp: pull_request.merged_at
)
end
end
end
end
end
end

View File

@ -1,57 +0,0 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
class PullRequestsMergedByImporter
include ParallelScheduling
def importer_class
PullRequestMergedByImporter
end
def representation_class
Gitlab::GithubImport::Representation::PullRequest
end
def sidekiq_worker_class
ImportPullRequestMergedByWorker
end
def collection_method
:pull_requests_merged_by
end
def object_type
:pull_request_merged_by
end
def id_for_already_imported_cache(merge_request)
merge_request.id
end
def each_object_to_import
merge_requests_to_import.find_each do |merge_request|
Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :fetched)
pull_request = client.pull_request(project.import_source, merge_request.iid)
yield(pull_request)
mark_as_imported(merge_request)
end
end
private
# Returns only the merge requests that still have merged_by to be imported.
def merge_requests_to_import
project.merge_requests.id_not_in(already_imported_objects).with_state(:merged)
end
def already_imported_objects
Gitlab::Cache::Import::Caching.values_from_set(already_imported_cache_key)
end
end
end
end
end

View File

@ -2375,6 +2375,9 @@ msgstr ""
msgid "AccountValidation|Verification is required to discourage and reduce the abuse on GitLab infrastructure. If you verify with a credit or debit card, %{strong_start}GitLab will not charge your card, it will only be used for validation.%{strong_end} %{learn_more_link}" msgid "AccountValidation|Verification is required to discourage and reduce the abuse on GitLab infrastructure. If you verify with a credit or debit card, %{strong_start}GitLab will not charge your card, it will only be used for validation.%{strong_end} %{learn_more_link}"
msgstr "" msgstr ""
msgid "Achievements"
msgstr ""
msgid "Achievements|%{namespace_full_path} awarded you the %{achievement_name} achievement" msgid "Achievements|%{namespace_full_path} awarded you the %{achievement_name} achievement"
msgstr "" msgstr ""

View File

@ -649,6 +649,8 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do
end end
it 'loads togglable usage ping payload on click', :js do it 'loads togglable usage ping payload on click', :js do
allow(Gitlab::Usage::ServicePingReport).to receive(:for).and_return({ uuid: '12345678', hostname: '127.0.0.1' })
stub_usage_data_connections stub_usage_data_connections
stub_database_flavor_check stub_database_flavor_check

View File

@ -1,7 +1,7 @@
/* eslint-disable no-return-assign, no-new, no-underscore-dangle */ /* eslint-disable no-return-assign, no-new, no-underscore-dangle */
import $ from 'jquery'; import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import htmlStaticLineHighlighter from 'test_fixtures_static/line_highlighter.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LineHighlighter from '~/blob/line_highlighter'; import LineHighlighter from '~/blob/line_highlighter';
import * as utils from '~/lib/utils/common_utils'; import * as utils from '~/lib/utils/common_utils';
@ -17,7 +17,7 @@ describe('LineHighlighter', () => {
}; };
beforeEach(() => { beforeEach(() => {
loadHTMLFixture('static/line_highlighter.html'); setHTMLFixture(htmlStaticLineHighlighter);
testContext.class = new LineHighlighter(); testContext.class = new LineHighlighter();
testContext.css = testContext.class.highlightLineClass; testContext.css = testContext.class.highlightLineClass;
return (testContext.spies = { return (testContext.spies = {

View File

@ -21,6 +21,7 @@ beforeEach(() => {
describe('CompareVersions', () => { describe('CompareVersions', () => {
let wrapper; let wrapper;
let store; let store;
let dispatchMock;
const targetBranchName = 'tmp-wine-dev'; const targetBranchName = 'tmp-wine-dev';
const { commit } = getDiffWithCommit; const { commit } = getDiffWithCommit;
@ -29,6 +30,8 @@ describe('CompareVersions', () => {
store.state.diffs.commit = { ...store.state.diffs.commit, ...commitArgs }; store.state.diffs.commit = { ...store.state.diffs.commit, ...commitArgs };
} }
dispatchMock = jest.spyOn(store, 'dispatch');
wrapper = mount(CompareVersionsComponent, { wrapper = mount(CompareVersionsComponent, {
store, store,
propsData: { propsData: {
@ -146,7 +149,7 @@ describe('CompareVersions', () => {
it('renders short commit ID', () => { it('renders short commit ID', () => {
expect(wrapper.text()).toContain('Viewing commit'); expect(wrapper.text()).toContain('Viewing commit');
expect(wrapper.text()).toContain(wrapper.vm.commit.short_id); expect(wrapper.text()).toContain(commit.short_id);
}); });
}); });
@ -204,10 +207,6 @@ describe('CompareVersions', () => {
setWindowLocation(`?commit_id=${mrCommit.id}`); setWindowLocation(`?commit_id=${mrCommit.id}`);
}); });
beforeEach(() => {
jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
});
it('uses the correct href', () => { it('uses the correct href', () => {
const link = getPrevCommitNavElement(); const link = getPrevCommitNavElement();
@ -219,7 +218,7 @@ describe('CompareVersions', () => {
link.trigger('click'); link.trigger('click');
await nextTick(); await nextTick();
expect(wrapper.vm.moveToNeighboringCommit).toHaveBeenCalledWith({ expect(dispatchMock).toHaveBeenCalledWith('diffs/moveToNeighboringCommit', {
direction: 'previous', direction: 'previous',
}); });
}); });
@ -238,10 +237,6 @@ describe('CompareVersions', () => {
setWindowLocation(`?commit_id=${mrCommit.id}`); setWindowLocation(`?commit_id=${mrCommit.id}`);
}); });
beforeEach(() => {
jest.spyOn(wrapper.vm, 'moveToNeighboringCommit').mockImplementation(() => {});
});
it('uses the correct href', () => { it('uses the correct href', () => {
const link = getNextCommitNavElement(); const link = getNextCommitNavElement();
@ -253,7 +248,9 @@ describe('CompareVersions', () => {
link.trigger('click'); link.trigger('click');
await nextTick(); await nextTick();
expect(wrapper.vm.moveToNeighboringCommit).toHaveBeenCalledWith({ direction: 'next' }); expect(dispatchMock).toHaveBeenCalledWith('diffs/moveToNeighboringCommit', {
direction: 'next',
});
}); });
it('renders a disabled button when there is no next commit', () => { it('renders a disabled button when there is no next commit', () => {

View File

@ -1,5 +1,5 @@
import prometheusIntegration from 'test_fixtures/integrations/prometheus/prometheus_integration.html';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import prometheusIntegration from 'test_fixtures/integrations/prometheus/prometheus_integration.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status';

View File

@ -1,5 +1,5 @@
import prometheusIntegration from 'test_fixtures/integrations/prometheus/prometheus_integration.html';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import prometheusIntegration from 'test_fixtures/integrations/prometheus/prometheus_integration.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';

View File

@ -10,8 +10,6 @@ describe('MenuSection component', () => {
const findButton = () => wrapper.find('button'); const findButton = () => wrapper.find('button');
const findCollapse = () => wrapper.getComponent(GlCollapse); const findCollapse = () => wrapper.getComponent(GlCollapse);
const findNavItems = () => wrapper.findAllComponents(NavItem); const findNavItems = () => wrapper.findAllComponents(NavItem);
const findSectionTitle = () => wrapper.findByTestId('section-title');
const createWrapper = (item, otherProps) => { const createWrapper = (item, otherProps) => {
wrapper = shallowMountExtended(MenuSection, { wrapper = shallowMountExtended(MenuSection, {
propsData: { item, ...otherProps }, propsData: { item, ...otherProps },
@ -70,17 +68,6 @@ describe('MenuSection component', () => {
}); });
}); });
describe('`collectionStyle` prop', () => {
const newClasses = 'gl-font-sm gl-font-weight-semibold'.split(' ');
it('applies new classes when using new styles', () => {
createWrapper({ title: 'Asdf' }, { collectionStyle: true });
const classes = findSectionTitle().classes();
newClasses.forEach((newClass) => expect(classes).toContain(newClass));
});
});
describe('`separated` prop', () => { describe('`separated` prop', () => {
describe('by default (false)', () => { describe('by default (false)', () => {
it('does not render a separator', () => { it('does not render a separator', () => {

View File

@ -1,6 +1,5 @@
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue'; import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue';
import MenuSection from '~/super_sidebar/components/menu_section.vue';
import { PANELS_WITH_PINS } from '~/super_sidebar/constants'; import { PANELS_WITH_PINS } from '~/super_sidebar/constants';
import { sidebarData } from '../mock_data'; import { sidebarData } from '../mock_data';
@ -102,10 +101,6 @@ describe('SidebarMenu component', () => {
'Also with subitems', 'Also with subitems',
]); ]);
}); });
it('passes `supportsPin` to menu sections', () => {
expect(wrapper.findAllComponents(MenuSection).at(1).props('collectionStyle')).toBe(true);
});
}); });
describe('when the sidebar does not support pins', () => { describe('when the sidebar does not support pins', () => {
@ -120,10 +115,6 @@ describe('SidebarMenu component', () => {
it('keeps all items as non-static', () => { it('keeps all items as non-static', () => {
expect(wrapper.vm.nonStaticItems).toEqual(menuItems); expect(wrapper.vm.nonStaticItems).toEqual(menuItems);
}); });
it('passes `supportsPin` to menu sections', () => {
expect(wrapper.findAllComponents(MenuSection).at(1).props('collectionStyle')).toBe(false);
});
}); });
}); });

View File

@ -0,0 +1,38 @@
import { mount } from '@vue/test-utils';
import SlotsWithSameName from './components/slots_with_same_name.vue';
import VOnceInsideVIf from './components/v_once_inside_v_if.vue';
import KeyInsideTemplate from './components/key_inside_template.vue';
import CommentsOnRootLevel from './components/comments_on_root_level.vue';
import SlotWithComment from './components/slot_with_comment.vue';
import DefaultSlotWithComment from './components/default_slot_with_comment.vue';
describe('Vue.js 3 compiler edge cases', () => {
it('workarounds issue #6063 when same slot is used with whitespace preserve', () => {
expect(() => mount(SlotsWithSameName)).not.toThrow();
});
it('workarounds issue #7725 when v-once is used inside v-if', () => {
expect(() => mount(VOnceInsideVIf)).not.toThrow();
});
it('renders vue.js 2 component when key is inside template', () => {
const wrapper = mount(KeyInsideTemplate);
expect(wrapper.text()).toBe('12345');
});
it('passes attributes to component with trailing comments on root level', () => {
const wrapper = mount(CommentsOnRootLevel, { propsData: { 'data-testid': 'test' } });
expect(wrapper.html()).toBe('<div data-testid="test"></div>');
});
it('treats empty slots with comments as empty', () => {
const wrapper = mount(SlotWithComment);
expect(wrapper.html()).toBe('<div>Simple</div>');
});
it('treats empty default slot with comments as empty', () => {
const wrapper = mount(DefaultSlotWithComment);
expect(wrapper.html()).toBe('<div>Simple</div>');
});
});

View File

@ -0,0 +1,5 @@
<template>
<!-- root level comment -->
<div><slot></slot></div>
<!-- root level comment -->
</template>

View File

@ -0,0 +1,18 @@
<script>
import Simple from './simple.vue';
export default {
components: {
Simple,
},
};
</script>
<template>
<simple>
<!-- slot comment typical for gitlab-ui, for example -->
<!-- slot comment typical for gitlab-ui, for example -->
<slot></slot>
<!-- slot comment typical for gitlab-ui, for example -->
<!-- slot comment typical for gitlab-ui, for example -->
</simple>
</template>

View File

@ -0,0 +1,7 @@
<template>
<div>
<template v-for="count in 5"
><span :key="count">{{ count }}</span></template
>
</div>
</template>

View File

@ -0,0 +1,10 @@
<script>
export default {
name: 'Simple',
};
</script>
<template>
<div>
<slot>{{ $options.name }}</slot>
</div>
</template>

View File

@ -0,0 +1,20 @@
<script>
import Simple from './simple.vue';
export default {
components: {
Simple,
},
};
</script>
<template>
<simple>
<template #default>
<!-- slot comment typical for gitlab-ui, for example -->
<!-- slot comment typical for gitlab-ui, for example -->
<slot></slot>
<!-- slot comment typical for gitlab-ui, for example -->
<!-- slot comment typical for gitlab-ui, for example -->
</template>
</simple>
</template>

View File

@ -0,0 +1,14 @@
<script>
import Simple from './simple.vue';
export default {
name: 'SlotsWithSameName',
components: { Simple },
};
</script>
<template>
<simple>
<template v-if="true" #default>{{ $options.name }}</template>
<template v-else #default>{{ $options.name }}</template>
</simple>
</template>

View File

@ -0,0 +1,12 @@
<script>
export default {
name: 'VOnceInsideVIf',
};
</script>
<template>
<div>
<template v-if="true">
<div v-once>{{ $options.name }}</div>
</template>
</div>
</template>

View File

@ -63,7 +63,7 @@ if (global.document) {
}; };
let compatH; let compatH;
Vue.config.compilerOptions.whitespace = 'condense'; Vue.config.compilerOptions.whitespace = 'preserve';
Vue.createApp({ Vue.createApp({
compatConfig: { compatConfig: {
MODE: 3, MODE: 3,

View File

@ -1,74 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_relative '../../config/initializers/doorkeeper_openid_connect_patch'
RSpec.describe 'doorkeeper_openid_connect_patch', feature_category: :integrations do
describe '.signing_key' do
let(:config) { Doorkeeper::OpenidConnect::Config.new }
before do
allow(config).to receive(:signing_key).and_return(key)
allow(config).to receive(:signing_algorithm).and_return(algorithm)
allow(Doorkeeper::OpenidConnect).to receive(:configuration).and_return(config)
end
context 'with RS256 algorithm' do
let(:algorithm) { :RS256 }
# Taken from https://github.com/doorkeeper-gem/doorkeeper-openid_connect/blob/01903c81a2b6237a3bf576ed45864f69ef20184e/spec/dummy/config/initializers/doorkeeper_openid_connect.rb#L6-L34
let(:key) do
<<~KEY
-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEAsjdnSA6UWUQQHf6BLIkIEUhMRNBJC1NN/pFt1EJmEiI88GS0
ceROO5B5Ooo9Y3QOWJ/n+u1uwTHBz0HCTN4wgArWd1TcqB5GQzQRP4eYnWyPfi4C
feqAHzQp+v4VwbcK0LW4FqtW5D0dtrFtI281FDxLhARzkhU2y7fuYhL8fVw5rUhE
8uwvHRZ5CEZyxf7BSHxIvOZAAymhuzNLATt2DGkDInU1BmF75tEtBJAVLzWG/j4L
PZh1EpSdfezqaXQlcy9PJi916UzTl0P7Yy+ulOdUsMlB6yo8qKTY1+AbZ5jzneHb
GDU/O8QjYvii1WDmJ60t0jXicmOkGrOhruOptwIDAQABAoIBAQChYNwMeu9IugJi
NsEf4+JDTBWMRpOuRrwcpfIvQAUPrKNEB90COPvCoju0j9OxCDmpdPtq1K/zD6xx
khlw485FVAsKufSp4+g6GJ75yT6gZtq1JtKo1L06BFFzb7uh069eeP7+wB6JxPHw
KlAqwxvsfADhxeolQUKCTMb3Vjv/Aw2cO/nn6RAOeftw2aDmFy8Xl+oTUtSxyib0
YCdU9cK8MxsxDdmowwHp04xRTm/wfG5hLEn7HMz1PP86iP9BiFsCqTId9dxEUTS1
K+VAt9FbxRAq5JlBocxUMHNxLigb94Ca2FOMR7F6l/tronLfHD801YoObF0fN9qW
Cgw4aTO5AoGBAOR79hiZVM7/l1cBid7hKSeMWKUZ/nrwJsVfNpu1H9xt9uDu+79U
mcGfM7pm7L2qCNGg7eeWBHq2CVg/XQacRNtcTlomFrw4tDXUkFN1hE56t1iaTs9m
dN9IDr6jFgf6UaoOxxoPT9Q1ZtO46l043Nzrkoz8cBEBaBY20bUDwCYjAoGBAMet
tt1ImGF1cx153KbOfjl8v54VYUVkmRNZTa1E821nL/EMpoONSqJmRVsX7grLyPL1
QyZe245NOvn63YM0ng0rn2osoKsMVJwYBEYjHL61iF6dPtW5p8FIs7auRnC3NrG0
XxHATZ4xhHD0iIn14iXh0XIhUVk+nGktHU1gbmVdAoGBANniwKdqqS6RHKBTDkgm
Dhnxw6MGa+CO3VpA1xGboxuRHeoY3KfzpIC5MhojBsZDvQ8zWUwMio7+w2CNZEfm
g99wYiOjyPCLXocrAssj+Rzh97AdzuQHf5Jh4/W2Dk9jTbdPSl02ltj2Z+2lnJFz
pWNjnqimHrSI09rDQi5NulJjAoGBAImquujVpDmNQFCSNA7NTzlTSMk09FtjgCZW
67cKUsqa2fLXRfZs84gD+s1TMks/NMxNTH6n57e0h3TSAOb04AM0kDQjkKJdXfhA
lrHEg4z4m4yf3TJ9Tat09HJ+tRIBPzRFp0YVz23Btg4qifiUDdcQWdbWIb/l6vCY
qhsu4O4BAoGBANbceYSDYRdT7a5QjJGibkC90Z3vFe4rDTBgZWg7xG0cpSU4JNg7
SFR3PjWQyCg7aGGXiooCM38YQruACTj0IFub24MFRA4ZTXvrACvpsVokJlQiG0Z4
tuQKYki41JvYqPobcq/rLE/AM7PKJftW35nqFuj0MrsUwPacaVwKBf5J
-----END RSA PRIVATE KEY-----
KEY
end
it 'returns the private key as JWK instance' do
expect(Doorkeeper::OpenidConnect.signing_key).to be_a ::JWT::JWK::KeyBase
expect(Doorkeeper::OpenidConnect.signing_key.kid).to eq 'IqYwZo2cE6hsyhs48cU8QHH4GanKIx0S4Dc99kgTIMA'
end
it 'matches json-jwt implementation' do
json_jwt_key = OpenSSL::PKey::RSA.new(key).public_key.to_jwk.slice(:kty, :kid, :e, :n)
expect(Doorkeeper::OpenidConnect.signing_key.export.sort.to_json).to eq(json_jwt_key.sort.to_json)
end
end
context 'with HS512 algorithm' do
let(:algorithm) { :HS512 }
let(:key) { 'the_greatest_secret_key' }
it 'returns the HMAC public key parameters' do
expect(Doorkeeper::OpenidConnect.signing_key_normalized).to eq(
kty: 'oct',
kid: 'lyAW7LdxryFWQtLdgxZpOrI87APHrzJKgWLT0BkWVog'
)
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::PullRequestsMergedByImporter do RSpec.describe Gitlab::GithubImport::Importer::PullRequests::AllMergedByImporter, feature_category: :importers do
let(:client) { double } let(:client) { double }
let_it_be(:project) { create(:project, import_source: 'http://somegithub.com') } let_it_be(:project) { create(:project, import_source: 'http://somegithub.com') }
@ -16,7 +16,11 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsMergedByImporter do
end end
describe '#importer_class' do describe '#importer_class' do
it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::PullRequestMergedByImporter) } it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::PullRequests::MergedByImporter) }
end
describe '#sidekiq_worker_class' do
it { expect(subject.sidekiq_worker_class).to eq(Gitlab::GithubImport::PullRequests::ImportMergedByWorker) }
end end
describe '#collection_method' do describe '#collection_method' do
@ -24,7 +28,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsMergedByImporter do
end end
describe '#id_for_already_imported_cache' do describe '#id_for_already_imported_cache' do
it { expect(subject.id_for_already_imported_cache(double(id: 1))).to eq(1) } it { expect(subject.id_for_already_imported_cache(instance_double(MergeRequest, id: 1))).to eq(1) }
end end
describe '#each_object_to_import', :clean_gitlab_redis_cache do describe '#each_object_to_import', :clean_gitlab_redis_cache do
@ -44,7 +48,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsMergedByImporter do
expect { |b| subject.each_object_to_import(&b) } expect { |b| subject.each_object_to_import(&b) }
.to yield_with_args(pull_request) .to yield_with_args(pull_request)
subject.each_object_to_import {} subject.each_object_to_import
end end
it 'skips cached merge requests' do it 'skips cached merge requests' do
@ -55,7 +59,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestsMergedByImporter do
expect(client).not_to receive(:pull_request) expect(client).not_to receive(:pull_request)
subject.each_object_to_import {} subject.each_object_to_import
end end
end end
end end

View File

@ -2,12 +2,16 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :clean_gitlab_redis_cache do RSpec.describe Gitlab::GithubImport::Importer::PullRequests::MergedByImporter,
:clean_gitlab_redis_cache, feature_category: :importers do
let_it_be(:merge_request) { create(:merged_merge_request) } let_it_be(:merge_request) { create(:merged_merge_request) }
let(:project) { merge_request.project } let(:project) { merge_request.project }
let(:merged_at) { Time.new(2017, 1, 1, 12, 00).utc } let(:merged_at) { Time.utc(2017, 1, 1, 12) }
let(:client_double) { double(user: { id: 999, login: 'merger', email: 'merger@email.com' } ) } let(:client_double) do
instance_double(Gitlab::GithubImport::Client, user: { id: 999, login: 'merger', email: 'merger@email.com' })
end
let(:merger_user) { { id: 999, login: 'merger' } } let(:merger_user) { { id: 999, login: 'merger' } }
let(:pull_request) do let(:pull_request) do
@ -25,7 +29,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :cle
shared_examples 'adds a note referencing the merger user' do shared_examples 'adds a note referencing the merger user' do
it 'adds a note referencing the merger user' do it 'adds a note referencing the merger user' do
expect { subject.execute } expect { subject.execute }
.to change(Note, :count).by(1) .to change { Note.count }.by(1)
.and not_change(merge_request, :updated_at) .and not_change(merge_request, :updated_at)
metrics = merge_request.metrics.reload metrics = merge_request.metrics.reload
@ -68,7 +72,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestMergedByImporter, :cle
it 'adds a note referencing the merger user' do it 'adds a note referencing the merger user' do
expect { subject.execute } expect { subject.execute }
.to change(Note, :count).by(1) .to change { Note.count }.by(1)
.and not_change(merge_request, :updated_at) .and not_change(merge_request, :updated_at)
metrics = merge_request.metrics.reload metrics = merge_request.metrics.reload

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::AchievementsController, feature_category: :user_profile do
let_it_be(:user) { create(:user) }
shared_examples 'response with 404 status' do
it 'returns 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
shared_examples 'ok response with index template' do
it 'renders the index template' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
shared_examples 'ok response with index template if authorized' do
context 'with a private group' do
let(:group) { create(:group, :private) }
context 'with authorized user' do
before do
group.add_guest(user)
sign_in(user)
end
it_behaves_like 'ok response with index template'
context 'when achievements ff is disabled' do
before do
stub_feature_flags(achievements: false)
end
it_behaves_like 'response with 404 status'
end
end
context 'with unauthorized user' do
before do
sign_in(user)
end
it_behaves_like 'response with 404 status'
end
context 'with anonymous user' do
it 'redirects to sign_in page' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(new_user_session_path)
end
end
end
context 'with a public group' do
let(:group) { create(:group, :public) }
context 'with anonymous user' do
it_behaves_like 'ok response with index template'
end
end
end
describe 'GET #index' do
subject { get group_achievements_path(group) }
it_behaves_like 'ok response with index template if authorized'
end
end

View File

@ -62,7 +62,7 @@ RSpec.describe Import::GithubFailureEntity, feature_category: :importers do
end end
it_behaves_like 'import failure entity' do it_behaves_like 'import failure entity' do
let(:source) { 'Gitlab::GithubImport::Importer::PullRequestMergedByImporter' } let(:source) { 'Gitlab::GithubImport::Importer::PullRequests::MergedByImporter' }
let(:title) { 'Pull request 2 merger' } let(:title) { 'Pull request 2 merger' }
let(:provider_url) { 'https://github.com/example/repo/pull/2' } let(:provider_url) { 'https://github.com/example/repo/pull/2' }
end end

View File

@ -277,6 +277,7 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do
'Gitlab::GithubImport::ImportPullRequestReviewWorker' => 5, 'Gitlab::GithubImport::ImportPullRequestReviewWorker' => 5,
'Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker' => 5, 'Gitlab::GithubImport::PullRequests::ImportReviewRequestWorker' => 5,
'Gitlab::GithubImport::PullRequests::ImportReviewWorker' => 5, 'Gitlab::GithubImport::PullRequests::ImportReviewWorker' => 5,
'Gitlab::GithubImport::PullRequests::ImportMergedByWorker' => 5,
'Gitlab::GithubImport::ImportPullRequestWorker' => 5, 'Gitlab::GithubImport::ImportPullRequestWorker' => 5,
'Gitlab::GithubImport::RefreshImportJidWorker' => 5, 'Gitlab::GithubImport::RefreshImportJidWorker' => 5,
'Gitlab::GithubImport::Stage::FinishImportWorker' => 5, 'Gitlab::GithubImport::Stage::FinishImportWorker' => 5,

View File

@ -10,6 +10,6 @@ RSpec.describe Gitlab::GithubImport::ImportPullRequestMergedByWorker, feature_ca
end end
describe '#importer_class' do describe '#importer_class' do
it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::PullRequestMergedByImporter) } it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::PullRequests::MergedByImporter) }
end end
end end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::PullRequests::ImportMergedByWorker, feature_category: :importers do
it { is_expected.to include_module(Gitlab::GithubImport::ObjectImporter) }
describe '#representation_class' do
it { expect(subject.representation_class).to eq(Gitlab::GithubImport::Representation::PullRequest) }
end
describe '#importer_class' do
it { expect(subject.importer_class).to eq(Gitlab::GithubImport::Importer::PullRequests::MergedByImporter) }
end
describe '#object_type' do
it { expect(subject.object_type).to eq(:pull_request_merged_by) }
end
end

View File

@ -13,7 +13,7 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportPullRequestsMergedByWorker, fe
client = double(:client) client = double(:client)
waiter = Gitlab::JobWaiter.new(2, '123') waiter = Gitlab::JobWaiter.new(2, '123')
expect(Gitlab::GithubImport::Importer::PullRequestsMergedByImporter) expect(Gitlab::GithubImport::Importer::PullRequests::AllMergedByImporter)
.to receive(:new) .to receive(:new)
.with(project, client) .with(project, client)
.and_return(importer) .and_return(importer)