Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-01-21 18:09:11 +00:00
parent 81884c35b8
commit 37a492326e
85 changed files with 1058 additions and 718 deletions

View File

@ -5,12 +5,14 @@ import mrEventHub from '../eventhub';
const CLASSES = {
opened: 'status-box-open',
locked: 'status-box-open',
closed: 'status-box-mr-closed',
merged: 'status-box-mr-merged',
};
const STATUS = {
opened: [__('Open'), 'issue-open-m'],
locked: [__('Open'), 'issue-open-m'],
closed: [__('Closed'), 'close'],
merged: [__('Merged'), 'git-merge'],
};

View File

@ -45,12 +45,18 @@ export default {
}
if (this.graphData?.maxValue) {
formatter = getFormatter(SUPPORTED_FORMATS.percent);
return formatter(this.queryResult / Number(this.graphData.maxValue), defaultPrecision);
formatter = getFormatter(SUPPORTED_FORMATS.number);
return formatter(
(this.queryResult / Number(this.graphData.maxValue)) * 100,
defaultPrecision,
);
}
formatter = getFormatter(SUPPORTED_FORMATS.number);
return `${formatter(this.queryResult, defaultPrecision)}${this.queryInfo.unit ?? ''}`;
return `${formatter(this.queryResult, defaultPrecision)}`;
},
unit() {
return this.graphData?.maxValue ? '%' : this.queryInfo.unit;
},
graphTitle() {
return this.queryInfo.label;
@ -60,6 +66,6 @@ export default {
</script>
<template>
<div>
<gl-single-stat :value="statValue" :title="graphTitle" variant="success" />
<gl-single-stat :value="statValue" :title="graphTitle" :unit="unit" variant="success" />
</div>
</template>

View File

@ -1,5 +1,7 @@
import { initCommitBoxInfo } from '~/projects/commit_box/info';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initCommitActions from '~/projects/commit';
initCommitBoxInfo();
initPipelines();
initCommitActions();

View File

@ -14,8 +14,7 @@ import flash from '~/flash';
import { __ } from '~/locale';
import loadAwardsHandler from '~/awards_handler';
import { initCommitBoxInfo } from '~/projects/commit_box/info';
import initRevertCommitTrigger from '~/projects/commit/init_revert_commit_trigger';
import initRevertCommitModal from '~/projects/commit/init_revert_commit_modal';
import initCommitActions from '~/projects/commit';
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
@ -47,5 +46,4 @@ if (filesContainer.length) {
new Diff();
}
loadAwardsHandler();
initRevertCommitModal();
initRevertCommitTrigger();
initCommitActions();

View File

@ -136,10 +136,13 @@ export default async function () {
createTestDetails();
createDagApp();
const canShowNewPipelineDetails =
gon.features.graphqlPipelineDetails || gon.features.graphqlPipelineDetailsUsers;
const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
let mediator;
if (!gon.features.graphqlPipelineHeader || !gon.features.graphqlPipelineDetails) {
if (!gon.features.graphqlPipelineHeader || !canShowNewPipelineDetails) {
try {
const { default: PipelinesMediator } = await import(
/* webpackChunkName: 'PipelinesMediator' */ './pipeline_details_mediator'
@ -151,7 +154,7 @@ export default async function () {
}
}
if (gon.features.graphqlPipelineDetails) {
if (canShowNewPipelineDetails) {
try {
const { createPipelinesDetailApp } = await import(
/* webpackChunkName: 'createPipelinesDetailApp' */ './pipeline_details_graph'

View File

@ -68,6 +68,7 @@ export default {
autocomplete="off"
:debounce="250"
:placeholder="$options.i18n.searchPlaceholder"
data-testid="dropdown-search-box"
@input="searchTermChanged"
/>
<gl-dropdown-item

View File

@ -10,6 +10,9 @@ export default {
displayText: {
default: '',
},
testId: {
default: '',
},
},
props: {
openModal: {
@ -26,7 +29,7 @@ export default {
</script>
<template>
<gl-link data-is-link="true" data-testid="revert-commit-link" @click="showModal">
<gl-link data-is-link="true" :data-testid="testId" @click="showModal">
{{ displayText }}
</gl-link>
</template>

View File

@ -2,6 +2,10 @@ import { s__, __ } from '~/locale';
export const OPEN_REVERT_MODAL = 'openRevertModal';
export const REVERT_MODAL_ID = 'revert-commit-modal';
export const REVERT_LINK_TEST_ID = 'revert-commit-link';
export const OPEN_CHERRY_PICK_MODAL = 'openCherryPickModal';
export const CHERRY_PICK_MODAL_ID = 'cherry-pick-commit-modal';
export const CHERRY_PICK_LINK_TEST_ID = 'cherry-pick-commit-link';
export const I18N_MODAL = {
startMergeRequest: s__('ChangeTypeAction|Start a %{newMergeRequest} with these changes'),
@ -20,6 +24,11 @@ export const I18N_REVERT_MODAL = {
actionPrimaryText: s__('ChangeTypeAction|Revert'),
};
export const I18N_CHERRY_PICK_MODAL = {
branchLabel: s__('ChangeTypeAction|Pick into branch'),
actionPrimaryText: s__('ChangeTypeAction|Cherry-pick'),
};
export const PREPENDED_MODAL_TEXT = s__(
'ChangeTypeAction|This will create a new commit in order to revert the existing changes.',
);

View File

@ -0,0 +1,11 @@
import initRevertCommitTrigger from './init_revert_commit_trigger';
import initRevertCommitModal from './init_revert_commit_modal';
import initCherryPickCommitTrigger from './init_cherry_pick_commit_trigger';
import initCherryPickCommitModal from './init_cherry_pick_commit_modal';
export default () => {
initRevertCommitModal();
initRevertCommitTrigger();
initCherryPickCommitModal();
initCherryPickCommitTrigger();
};

View File

@ -0,0 +1,51 @@
import Vue from 'vue';
import CommitFormModal from './components/form_modal.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import createStore from './store';
import {
I18N_MODAL,
I18N_CHERRY_PICK_MODAL,
OPEN_CHERRY_PICK_MODAL,
CHERRY_PICK_MODAL_ID,
} from './constants';
export default function initInviteMembersModal() {
const el = document.querySelector('.js-cherry-pick-commit-modal');
if (!el) {
return false;
}
const {
title,
endpoint,
branch,
pushCode,
branchCollaboration,
existingBranch,
branchesEndpoint,
} = el.dataset;
const store = createStore({
endpoint,
branchesEndpoint,
branch,
pushCode: parseBoolean(pushCode),
branchCollaboration: parseBoolean(branchCollaboration),
defaultBranch: branch,
modalTitle: title,
existingBranch,
});
return new Vue({
el,
store,
render: (createElement) =>
createElement(CommitFormModal, {
props: {
i18n: { ...I18N_CHERRY_PICK_MODAL, ...I18N_MODAL },
openModal: OPEN_CHERRY_PICK_MODAL,
modalId: CHERRY_PICK_MODAL_ID,
},
}),
});
}

View File

@ -0,0 +1,20 @@
import Vue from 'vue';
import CommitFormTrigger from './components/form_trigger.vue';
import { OPEN_CHERRY_PICK_MODAL, CHERRY_PICK_LINK_TEST_ID } from './constants';
export default function initInviteMembersTrigger() {
const el = document.querySelector('.js-cherry-pick-commit-trigger');
if (!el) {
return false;
}
const { displayText } = el.dataset;
return new Vue({
el,
provide: { displayText, testId: CHERRY_PICK_LINK_TEST_ID },
render: (createElement) =>
createElement(CommitFormTrigger, { props: { openModal: OPEN_CHERRY_PICK_MODAL } }),
});
}

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import RevertCommitTrigger from './components/form_trigger.vue';
import { OPEN_REVERT_MODAL } from './constants';
import CommitFormTrigger from './components/form_trigger.vue';
import { OPEN_REVERT_MODAL, REVERT_LINK_TEST_ID } from './constants';
export default function initInviteMembersTrigger() {
const el = document.querySelector('.js-revert-commit-trigger');
@ -13,8 +13,8 @@ export default function initInviteMembersTrigger() {
return new Vue({
el,
provide: { displayText },
provide: { displayText, testId: REVERT_LINK_TEST_ID },
render: (createElement) =>
createElement(RevertCommitTrigger, { props: { openModal: OPEN_REVERT_MODAL } }),
createElement(CommitFormTrigger, { props: { openModal: OPEN_REVERT_MODAL } }),
});
}

View File

@ -15,6 +15,16 @@ export default {
type: Object,
},
},
i18n: {
changes: s__(
'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
),
generationErrored: s__('Terraform|Generating the report caused an error.'),
namedReportFailed: s__('Terraform|The report %{name} failed to generate.'),
namedReportGenerated: s__('Terraform|The report %{name} was generated in your pipelines.'),
reportFailed: s__('Terraform|A report failed to generate.'),
reportGenerated: s__('Terraform|A report was generated in your pipelines.'),
},
computed: {
addNum() {
return Number(this.plan.create);
@ -30,23 +40,21 @@ export default {
},
reportChangeText() {
if (this.validPlanValues) {
return s__(
'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
);
return this.$options.i18n.changes;
}
return s__('Terraform|Generating the report caused an error.');
return this.$options.i18n.generationErrored;
},
reportHeaderText() {
if (this.validPlanValues) {
return this.plan.job_name
? s__('Terraform|The Terraform report %{name} was generated in your pipelines.')
: s__('Terraform|A Terraform report was generated in your pipelines.');
? this.$options.i18n.namedReportGenerated
: this.$options.i18n.reportGenerated;
}
return this.plan.job_name
? s__('Terraform|The Terraform report %{name} failed to generate.')
: s__('Terraform|A Terraform report failed to generate.');
? this.$options.i18n.namedReportFailed
: this.$options.i18n.reportFailed;
},
validPlanValues() {
return this.addNum + this.changeNum + this.deleteNum >= 0;

View File

@ -84,7 +84,7 @@ export const tributeConfig = {
value.type === groupType ? last(value.name.split(' / ')) : `${value.name}${value.username}`,
menuItemLimit: memberLimit,
menuItemTemplate: ({ original }) => {
const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0';
const commonClasses = 'gl-avatar gl-avatar-s32 gl-flex-shrink-0';
const noAvatarClasses = `${commonClasses} gl-rounded-small
gl-display-flex gl-align-items-center gl-justify-content-center`;
@ -111,7 +111,7 @@ export const tributeConfig = {
return `
<div class="gl-display-flex gl-align-items-center">
${avatar}
<div class="gl-font-sm gl-line-height-normal gl-ml-3">
<div class="gl-line-height-normal gl-ml-4">
<div>${escape(displayName)}${count}</div>
<div class="gl-text-gray-700">${escape(parentGroupOrUsername)}</div>
</div>

View File

@ -1,13 +1,10 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlLink, GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue';
import { LOADING, ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
import { s__ } from '~/locale';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import Api from '~/api';
import HelpIcon from './components/help_icon.vue';
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
import SecuritySummary from './components/security_summary.vue';
@ -24,8 +21,6 @@ import { extractSecurityReportArtifacts } from './utils';
export default {
store,
components: {
GlLink,
GlSprintf,
ReportSection,
HelpIcon,
SecurityReportDownloadDropdown,
@ -101,9 +96,6 @@ export default {
),
};
},
skip() {
return !this.canShowDownloads;
},
update(data) {
return extractSecurityReportArtifacts(this.$options.reportTypes, data);
},
@ -124,9 +116,6 @@ export default {
},
computed: {
...mapGetters(['groupedSummaryText', 'summaryStatus']),
canShowDownloads() {
return this.glFeatures.coreSecurityMrWidgetDownloads;
},
hasSecurityReports() {
return this.availableSecurityReports.length > 0;
},
@ -139,23 +128,6 @@ export default {
isLoadingReportArtifacts() {
return this.$apollo.queries.reportArtifacts.loading;
},
shouldShowDownloadGuidance() {
return !this.canShowDownloads && this.summaryStatus !== LOADING;
},
scansHaveRunMessage() {
return this.canShowDownloads
? this.$options.i18n.scansHaveRun
: this.$options.i18n.scansHaveRunWithDownloadGuidance;
},
},
created() {
if (!this.canShowDownloads) {
this.checkAvailableSecurityReports(this.$options.reportTypes)
.then((availableSecurityReports) => {
this.onCheckingAvailableSecurityReports(Array.from(availableSecurityReports));
})
.catch(this.showError);
}
},
methods: {
...mapActions(MODULE_SAST, {
@ -166,36 +138,6 @@ export default {
setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
fetchSecretDetectionDiff: 'fetchDiff',
}),
async checkAvailableSecurityReports(reportTypes) {
const reportTypesSet = new Set(reportTypes);
const availableReportTypes = new Set();
let page = 1;
while (page) {
// eslint-disable-next-line no-await-in-loop
const { data: jobs, headers } = await Api.pipelineJobs(this.projectId, this.pipelineId, {
per_page: 100,
page,
});
jobs.forEach(({ artifacts = [] }) => {
artifacts.forEach(({ file_type }) => {
if (reportTypesSet.has(file_type)) {
availableReportTypes.add(file_type);
}
});
});
// If we've found artifacts for all the report types, stop looking!
if (availableReportTypes.size === reportTypesSet.size) {
return availableReportTypes;
}
page = parseIntPagination(normalizeHeaders(headers)).nextPage;
}
return availableReportTypes;
},
fetchCounts() {
if (!this.glFeatures.coreSecurityMrWidgetCounts) {
return;
@ -213,11 +155,6 @@ export default {
this.canShowCounts = true;
}
},
activatePipelinesTab() {
if (window.mrTabs) {
window.mrTabs.tabShown('pipelines');
}
},
onCheckingAvailableSecurityReports(availableSecurityReports) {
this.availableSecurityReports = availableSecurityReports;
this.fetchCounts();
@ -236,12 +173,6 @@ export default {
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
),
scansHaveRun: s__('SecurityReports|Security scans have run'),
scansHaveRunWithDownloadGuidance: s__(
'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
downloadFromPipelineTab: s__(
'SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
},
summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
};
@ -265,22 +196,7 @@ export default {
</span>
</template>
<template v-if="shouldShowDownloadGuidance" #sub-heading>
<span class="gl-font-sm">
<gl-sprintf :message="$options.i18n.downloadFromPipelineTab">
<template #link="{ content }">
<gl-link
class="gl-font-sm"
data-testid="show-pipelines"
@click="activatePipelinesTab"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</span>
</template>
<template v-if="canShowDownloads" #action-buttons>
<template #action-buttons>
<security-report-download-dropdown
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
@ -298,13 +214,7 @@ export default {
data-testid="security-mr-widget"
>
<template #error>
<gl-sprintf :message="scansHaveRunMessage">
<template #link="{ content }">
<gl-link data-testid="show-pipelines" @click="activatePipelinesTab">{{
content
}}</gl-link>
</template>
</gl-sprintf>
{{ $options.i18n.scansHaveRun }}
<help-icon
:help-path="securityReportsDocsPath"
@ -312,7 +222,7 @@ export default {
/>
</template>
<template v-if="canShowDownloads" #action-buttons>
<template #action-buttons>
<security-report-download-dropdown
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"

View File

@ -38,7 +38,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
push_frontend_feature_flag(:core_security_mr_widget_downloads, @project, default_enabled: true)
push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
push_frontend_feature_flag(:diffs_gradual_load, @project, default_enabled: true)
push_frontend_feature_flag(:codequality_mr_diff, @project)

View File

@ -16,7 +16,8 @@ class Projects::PipelinesController < Projects::ApplicationController
push_frontend_feature_flag(:pipelines_security_report_summary, project)
push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: true)
push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false)
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: false)
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development, default_enabled: true)
end
before_action :ensure_pipeline, only: [:show]

View File

@ -110,20 +110,16 @@ module CommitsHelper
end
end
def revert_commit_link(commit, continue_to_path, btn_class: nil, pajamas: false)
def revert_commit_link
return unless current_user
action = 'revert'
if pajamas && can_collaborate_with_project?(@project)
tag(:div, data: { display_text: action.capitalize }, class: "js-revert-commit-trigger")
else
commit_action_link(action, commit, continue_to_path, btn_class: btn_class, has_tooltip: false)
end
tag(:div, data: { display_text: 'Revert' }, class: "js-revert-commit-trigger")
end
def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip)
def cherry_pick_commit_link
return unless current_user
tag(:div, data: { display_text: 'Cherry-pick' }, class: "js-cherry-pick-commit-trigger")
end
def commit_signature_badge_classes(additional_classes)
@ -143,7 +139,7 @@ module CommitsHelper
def commit_person_link(commit, options = {})
user = commit.public_send(options[:source]) # rubocop:disable GitlabSecurity/PublicSend
source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend
source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend
source_email = clean(commit.public_send(:"#{options[:source]}_email")) # rubocop:disable GitlabSecurity/PublicSend
person_name = user.try(:name) || source_name
@ -166,28 +162,6 @@ module CommitsHelper
end
end
def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true)
return unless current_user
tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip
btn_class = "btn btn-#{btn_class}" unless btn_class.nil?
if can_collaborate_with_project?(@project)
link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}"
elsif can?(current_user, :fork_project, @project)
continue_params = {
to: continue_to_path,
notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.",
notice_now: edit_in_new_fork_notice_now
}
fork_path = project_forks_path(@project,
namespace_key: current_user.namespace.id,
continue: continue_params)
link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip)
end
end
def view_file_button(commit_sha, diff_new_path, project, replaced: false)
path = project_blob_path(project, tree_join(commit_sha, diff_new_path))
title = replaced ? _('View replaced file @ ') : _('View file @ ')

View File

@ -4,6 +4,8 @@ module Pages
class LookupPath
include Gitlab::Utils::StrongMemoize
LegacyStorageDisabledError = Class.new(::StandardError)
def initialize(project, trim_prefix: nil, domain: nil)
@project = project
@domain = domain
@ -24,7 +26,7 @@ module Pages
end
def source
zip_source || file_source
zip_source || legacy_source
end
def prefix
@ -64,11 +66,17 @@ module Pages
}
end
def file_source
def legacy_source
raise LegacyStorageDisabledError unless Feature.enabled?(:pages_serve_from_legacy_storage, default_enabled: true)
{
type: 'file',
path: File.join(project.full_path, 'public/')
}
rescue LegacyStorageDisabledError => e
Gitlab::ErrorTracking.track_exception(e)
nil
end
end
end

View File

@ -17,9 +17,16 @@ module Pages
end
def lookup_paths
projects.map do |project|
paths = projects.map do |project|
project.pages_lookup_path(trim_prefix: trim_prefix, domain: domain)
end.sort_by(&:prefix).reverse
end
# TODO: remove in https://gitlab.com/gitlab-org/gitlab/-/issues/297524
# source can only be nil if pages_serve_from_legacy_storage FF is disabled
# we can remove this filtering once we remove legacy storage
paths = paths.select(&:source)
paths.sort_by(&:prefix).reverse
end
private

View File

@ -22,4 +22,14 @@
- label = s_('ChangeTypeAction|Cherry-pick')
- branch_label = s_('ChangeTypeActionLabel|Pick into branch')
- title = commit.merged_merge_request(current_user) ? _('Cherry-pick this merge request') : _('Cherry-pick this commit')
= render "projects/commit/commit_modal", title: title, type: type, commit: commit, branch_label: branch_label, description: description, label: label
- if defined?(pajamas)
.js-cherry-pick-commit-modal{ data: { title: title,
endpoint: cherry_pick_namespace_project_commit_path(commit, namespace_id: @project.namespace.full_path, project_id: @project),
branch: @project.default_branch,
push_code: can?(current_user, :push_code, @project).to_s,
branch_collaboration: @project.branch_allows_collaboration?(current_user, selected_branch).to_s,
existing_branch: ERB::Util.html_escape(selected_branch),
branches_endpoint: project_branches_path(@project) } }
- else
= render "projects/commit/commit_modal", title: title, type: type, commit: commit, branch_label: branch_label, description: description, label: label

View File

@ -37,10 +37,10 @@
#{ _('Browse Files') }
- if can_collaborate && !@commit.has_been_reverted?(current_user)
%li.clearfix
= revert_commit_link(@commit, project_commit_path(@project, @commit.id), pajamas: true)
= revert_commit_link
- if can_collaborate
%li.clearfix
= cherry_pick_commit_link(@commit, project_commit_path(@project, @commit.id), has_tooltip: false)
= cherry_pick_commit_link
- if can?(current_user, :push_code, @project)
%li.clearfix
= link_to s_('CreateTag|Tag'), new_project_tag_path(@project, ref: @commit)

View File

@ -4,3 +4,7 @@
= render 'commit_box'
= render 'ci_menu'
= render 'projects/commit/pipelines_list', endpoint: pipelines_project_commit_path(@project, @commit.id)
- if can_collaborate_with_project?(@project)
= render "projects/commit/change", type: 'revert', commit: @commit, pajamas: true
= render "projects/commit/change", type: 'cherry-pick', commit: @commit, pajamas: true

View File

@ -18,4 +18,4 @@
= render "shared/notes/notes_with_form", :autocomplete => true
- if can_collaborate_with_project?(@project)
= render "projects/commit/change", type: 'revert', commit: @commit, pajamas: true
= render "projects/commit/change", type: 'cherry-pick', commit: @commit, title: @commit.title
= render "projects/commit/change", type: 'cherry-pick', commit: @commit, pajamas: true

View File

@ -6,7 +6,7 @@
- add_page_specific_style 'page_bundles/reports'
- add_page_specific_style 'page_bundles/ci_status'
- if Feature.enabled?(:graphql_pipeline_details, @project)
- if Feature.enabled?(:graphql_pipeline_details, @project, default_enabled: :yaml) || Feature.enabled?(:graphql_pipeline_details_users, @current_user, default_enabled: :yaml)
- add_page_startup_graphql_call('pipelines/get_pipeline_details', { projectPath: @project.full_path, iid: @pipeline.iid })
.js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } }

View File

@ -25,6 +25,7 @@ module ContainerExpirationPolicies
return unless container_repository
log_extra_metadata_on_done(:container_repository_id, container_repository.id)
log_extra_metadata_on_done(:project_id, project.id)
unless allowed_to_run?(container_repository)
container_repository.cleanup_unscheduled!
@ -78,7 +79,7 @@ module ContainerExpirationPolicies
end
def project
container_repository&.project
container_repository.project
end
def container_repository

View File

@ -0,0 +1,5 @@
---
title: "[Commit Page] Migrate to GlModal for cherry-pick commit"
merge_request: 51650
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Add On-call Rotations destroy mutation to GraphQL
merge_request: 51860
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Remove redundant text in Terraform Widget
merge_request: 52013
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Database migration for compliance pipeline configuration location
merge_request: 51663
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fixed merge requests locked status not showing
merge_request: 52078
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Support Project variables in `include` section of `gitlab-ci.yml`
merge_request: 52108
author:
type: added

View File

@ -1,8 +0,0 @@
---
name: core_security_mr_widget_downloads
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48769
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273418
milestone: '13.7'
type: development
group: group::static analysis
default_enabled: true

View File

@ -1,8 +1,8 @@
---
name: drop_license_management_artifact
introduced_by_url:
rollout_issue_url:
milestone:
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31247
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299114
milestone: 13.0
type: development
group: group::composition analysis
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: graphql_pipeline_details_users
introduced_by_url:
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299112
milestone: '13.9'
type: development
group: group::pipeline authoring
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: pages_serve_from_legacy_storage
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297228
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297524
milestone: '13.8'
type: development
group: group::release
default_enabled: true

View File

@ -1,8 +0,0 @@
---
name: variables_in_include_section_ci
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50188/
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/294294
milestone: '13.8'
type: development
group: group::compliance
default_enabled: false

View File

@ -0,0 +1,83 @@
---
- title: Pipeline editor
body: |
GitLab CI/CD provides you with flexible options to support a variety of advanced pipeline use cases. Pipeline syntax can be verbose and sometimes complicated, especially for those who are new to GitLab CI/CD. In this release, we are proud to introduce our first iteration of the Pipeline Editor.
The editor makes the CI configuration authoring experience much easier for both novice and advanced users alike. The pipeline editor is a single solution that groups all the existing CI authoring features (and future ones) in a single location. The pipeline editor is the best place to go when configuring your pipeline.
stage: Verify
self-managed: true
gitlab-com: true
packages: [Core, Starter, Premium, Ultimate]
url: https://docs.gitlab.com/ee/ci/pipeline_editor/index.html
image_url: https://img.youtube.com/vi/MQpSyvMpsHA/hqdefault.jpg
published_at: 2021-01-22
release: 13.8
- title: Visualization of pipeline configuration
body: |
A complex CI configuration can be difficult to understand as a developer, especially when trying to predict how your pipeline might behave (or misbehave). Without a visual aid, it is challenging to form a mental image of the relationships between all of the jobs and determine how they are interconnected. In our first iteration of a pipeline visualization, you can now see a graphic representation of your `.gitlab-ci.yml` configuration to better understand and predict how your pipelines will perform.
stage: verify
self-managed: true
gitlab-com: true
packages: [Core, Starter, Premium, Ultimate]
url: https://docs.gitlab.com/ee/ci/yaml/visualization.html
image_url: https://about.gitlab.com/images/13_8/pipeline_visual_builder.png
published_at: 2021-01-22
release: 13.8
- title: Deployment frequency charts
packages: [Ultimate]
self-managed: true
gitlab-com: true
image_url: https://about.gitlab.com/images/13_8/deployment_graph.png
url: https://docs.gitlab.com/ee/user/analytics/ci_cd_analytics.html#deployment-frequency-charts
stage: Release
body: |
Knowing and monitoring deployment frequency is a starting point for organizations adopting DevOps. We are proud to introduce the deployment frequency charts at the project level so that you and your development teams can monitor the efficiency of deployments over time, find bottlenecks, and make improvements when necessary. This is the first of the DORA metrics that we are making available within GitLab out of the box.
published_at: 2021-01-22
release: 13.8
- title: Scope a board to the current iteration
body: |
Many teams use boards to manage issues during an iteration. In 13.6, we added support for [filtering issues on a board to a specific Iteration](https://gitlab.com/gitlab-org/gitlab/-/issues/118742), but it is cumbersome to remember to apply that filter every time you go to your board. In this release, we've added the ability to scope your board to the currently active iteration.
stage: plan
self-managed: true
gitlab-com: true
packages: [Starter, Premium, Ultimate]
url: https://docs.gitlab.com/ee/user/project/issue_board.html#configurable-issue-boards
image_url: https://about.gitlab.com/images/13_8/scope_board_to_current_iteration.png
published_at: 2021-01-22
release: 13.8
- title: Download artifacts directly from the merge request widget
body: |
We added the ability to download build artifacts directly from the MR widget. This is especially useful for mobile development. An example of this is where users want to test an Android package of a specific build created on a physical device or an emulator. You can now access these artifacts directly from the merge request widget without having to find the artifacts buried in the pipeline view.
stage: Release
self-managed: true
gitlab-com: true
packages: [Core, Starter, Premium, Ultimate]
url: https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html#downloading-artifacts
image_url: https://about.gitlab.com/images/13_8/artifact_mr.png
published_at: 2021-01-22
release: 13.8
- title: Rebase quick action for merge requests
body: |
Rebase is a Git command used to reapply commits on top of a new commit. In practice, this means reapplying commits from a feature branch on top of the latest version of the target branch (e.g. `main`). In this way, it is possible to bring the feature branch up to date and resolve any conflicts without using a merge commit with the benefit of a simpler linear Git history.
GitLab 13.8 brings the ability to execute the rebase quick action in merge requests, allowing you to quickly invoke the rebase Git utility.
stage: create
self-managed: true
gitlab-com: true
packages: [Core, Starter, Premium, Ultimate]
url: https://docs.gitlab.com/ee/user/project/quick_actions.html#quick-actions-for-issues-merge-requests-and-epics
image_url: https://about.gitlab.com/images/13_8/rebase_comment.png
published_at: 2021-01-22
release: 13.8
- title: GitLab Pages is now available for Kubernetes deployments of GitLab
body: |
GitLab Pages is a popular static-site hosting service built into GitLab, and we are excited to announce that it is now available for GitLab instances running on Kubernetes. Pages was one of the last remaining feature gaps compared to an Omnibus deployment.
stage: enablement
self-managed: true
gitlab-com: false
packages: [Core, Starter, Premium, Ultimate]
url: https://docs.gitlab.com/charts/charts/gitlab/gitlab-pages/index.html
image_url: https://about.gitlab.com/images/home/kubernetes.png
published_at: 2021-01-22
release: 13.8

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddPipelineConfigurationFullPathToCompliancePipeline < ActiveRecord::Migration[6.0]
DOWNTIME = false
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20210119162812_add_text_limit_to_compliance_pipeline_configuration_full_path.rb
def up
add_column :compliance_management_frameworks, :pipeline_configuration_full_path, :text
end
# rubocop:enable Migration/AddLimitToTextColumns
def down
remove_column :compliance_management_frameworks, :pipeline_configuration_full_path
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddTextLimitToCompliancePipelineConfigurationFullPath < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_text_limit :compliance_management_frameworks, :pipeline_configuration_full_path, 255
end
def down
remove_text_limit :compliance_management_frameworks, :pipeline_configuration_full_path
end
end

View File

@ -0,0 +1 @@
e6841491cd7d2cc015fd628f5c14270720d59cbb17b7efb160937963f074f5c2

View File

@ -0,0 +1 @@
cd7643fc762d8b9236ef5ac7cc285ffbd29f1953178b9b6e129082efd7b9e07b

View File

@ -11457,9 +11457,11 @@ CREATE TABLE compliance_management_frameworks (
color text NOT NULL,
namespace_id integer NOT NULL,
regulated boolean DEFAULT true NOT NULL,
pipeline_configuration_full_path text,
CONSTRAINT check_08cd34b2c2 CHECK ((char_length(color) <= 10)),
CONSTRAINT check_1617e0b87e CHECK ((char_length(description) <= 255)),
CONSTRAINT check_ab00bc2193 CHECK ((char_length(name) <= 255))
CONSTRAINT check_ab00bc2193 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_e7a9972435 CHECK ((char_length(pipeline_configuration_full_path) <= 255))
);
CREATE SEQUENCE compliance_management_frameworks_id_seq

View File

@ -173,7 +173,7 @@ successfully, you must replicate their data using some other means.
| Feature | Replicated (added in GitLab version) | Verified (added in GitLab version) | Object Storage replication (see [Geo with Object Storage](object_storage.md)) | Notes |
|:---------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------|:----------------------------------------------------------|:-------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [Application data in PostgreSQL](../../postgresql/index.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
| [Project repository](../../..//user/project/repository/) | **Yes** (10.2) | **Yes** (10.7) | No | |
| [Project repository](../../../user/project/repository/) | **Yes** (10.2) | **Yes** (10.7) | No | |
| [Project wiki repository](../../../user/project/wiki/) | **Yes** (10.2) | **Yes** (10.7) | No |
| [Group wiki repository](../../../user/group/index.md#group-wikis) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/208147) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/208147) | No | |
| [Uploads](../../uploads.md) | **Yes** (10.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | No | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both nodes and comparing the output between them. |

View File

@ -472,7 +472,7 @@ information, see [High Availability with Omnibus GitLab](../../postgresql/replic
## Patroni support
Support for Patroni is intended to replace `repmgr` as a
[highly availabile PostgreSQL solution](../../postgresql/replication_and_failover.md)
[highly available PostgreSQL solution](../../postgresql/replication_and_failover.md)
on the primary node, but it can also be used for PostgreSQL HA on a secondary
node.

View File

@ -3808,6 +3808,12 @@ type ComplianceFramework {
Name of the compliance framework
"""
name: String!
"""
Full path of the compliance pipeline configuration stored in a project
repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`.
"""
pipelineConfigurationFullPath: String
}
"""
@ -3860,6 +3866,12 @@ input ComplianceFrameworkInput {
New name for the compliance framework.
"""
name: String
"""
Full path of the compliance pipeline configuration stored in a project
repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`.
"""
pipelineConfigurationFullPath: String
}
"""
@ -15836,6 +15848,7 @@ type Mutation {
mergeRequestUpdate(input: MergeRequestUpdateInput!): MergeRequestUpdatePayload
namespaceIncreaseStorageTemporarily(input: NamespaceIncreaseStorageTemporarilyInput!): NamespaceIncreaseStorageTemporarilyPayload
oncallRotationCreate(input: OncallRotationCreateInput!): OncallRotationCreatePayload
oncallRotationDestroy(input: OncallRotationDestroyInput!): OncallRotationDestroyPayload
oncallScheduleCreate(input: OncallScheduleCreateInput!): OncallScheduleCreatePayload
oncallScheduleDestroy(input: OncallScheduleDestroyInput!): OncallScheduleDestroyPayload
oncallScheduleUpdate(input: OncallScheduleUpdateInput!): OncallScheduleUpdatePayload
@ -16597,6 +16610,51 @@ input OncallRotationDateInputType {
time: String!
}
"""
Autogenerated input type of OncallRotationDestroy
"""
input OncallRotationDestroyInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The ID of the on-call rotation to remove.
"""
id: IncidentManagementOncallRotationID!
"""
The project to remove the on-call schedule from.
"""
projectPath: ID!
"""
The IID of the on-call schedule to the on-call rotation belongs to.
"""
scheduleIid: String!
}
"""
Autogenerated return type of OncallRotationDestroy
"""
type OncallRotationDestroyPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The on-call rotation.
"""
oncallRotation: IncidentManagementOncallRotation
}
"""
The rotation length of the on-call rotation
"""

View File

@ -10370,6 +10370,20 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pipelineConfigurationFullPath",
"description": "Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@ -10526,6 +10540,16 @@
"ofType": null
},
"defaultValue": null
},
{
"name": "pipelineConfigurationFullPath",
"description": "Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
@ -45793,6 +45817,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "oncallRotationDestroy",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "OncallRotationDestroyInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "OncallRotationDestroyPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "oncallScheduleCreate",
"description": null,
@ -49118,6 +49169,136 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "OncallRotationDestroyInput",
"description": "Autogenerated input type of OncallRotationDestroy",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project to remove the on-call schedule from.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "scheduleIid",
"description": "The IID of the on-call schedule to the on-call rotation belongs to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "id",
"description": "The ID of the on-call rotation to remove.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "IncidentManagementOncallRotationID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "OncallRotationDestroyPayload",
"description": "Autogenerated return type of OncallRotationDestroy",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "oncallRotation",
"description": "The on-call rotation.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "IncidentManagementOncallRotation",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "OncallRotationLengthInputType",

View File

@ -591,6 +591,7 @@ Represents a ComplianceFramework associated with a Project.
| `description` | String! | Description of the compliance framework |
| `id` | ID! | Compliance framework ID |
| `name` | String! | Name of the compliance framework |
| `pipelineConfigurationFullPath` | String | Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/compliance/soc2/.gitlab-ci.yml`. |
### ConfigureSastPayload
@ -2494,6 +2495,16 @@ Autogenerated return type of OncallRotationCreate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `oncallRotation` | IncidentManagementOncallRotation | The on-call rotation. |
### OncallRotationDestroyPayload
Autogenerated return type of OncallRotationDestroy.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `oncallRotation` | IncidentManagementOncallRotation | The on-call rotation. |
### OncallScheduleCreatePayload
Autogenerated return type of OncallScheduleCreate.

View File

@ -1406,7 +1406,7 @@ Parameters:
| `merge_request_iid` | integer | yes | The internal ID of the merge request. |
| `merge_commit_message` | string | no | Custom merge commit message. |
| `squash_commit_message` | string | no | Custom squash commit message. |
| `squash` | boolean | no | If `true` the commits the commits are squashed into a single commit on merge. |
| `squash` | boolean | no | If `true` the commits are squashed into a single commit on merge. |
| `should_remove_source_branch` | boolean | no | If `true` removes the source branch. |
| `merge_when_pipeline_succeeds` | boolean | no | If `true` the MR is merged when the pipeline succeeds. |
| `sha` | string | no | If present, then this SHA must match the HEAD of the source branch, otherwise the merge fails. |

View File

@ -375,7 +375,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
## Require code owner approvals for a single branch
Update the "code owner approval required" option for the given protected branch protected branch.
Update the "code owner approval required" option for the given protected branch.
```plaintext
PATCH /projects/:id/protected_branches/:name

View File

@ -575,7 +575,7 @@ Example response:
```json
{
"id": "12345",
"id": 12345,
"token": "6337ff461c94fd3fa32ba3b1ff4125"
}
```

View File

@ -10,7 +10,7 @@ type: reference
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4540) in GitLab 13.8.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It's not recommended for production use.
> - It's recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-pipeline-editor). **(CORE ONLY)**
WARNING:
@ -62,7 +62,7 @@ reflected in the CI lint. It displays the same results as the existing [CI Lint
> - It was [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/290117) in GitLab 13.8.
> - It's enabled on GitLab.com.
> - It's not recommended for production use.
> - It's recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-cicd-configuration-visualization). **(CORE ONLY)**
WARNING:
@ -115,7 +115,7 @@ checkbox appears. Select it to start a new merge request after you commit the ch
## Enable or disable pipeline editor **(CORE ONLY)**
The pipeline editor is under development and not ready for production use. It is
The pipeline editor is under development but ready for production use. It is
deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can disable it.

View File

@ -161,7 +161,7 @@ Try to find which jobs don't need to run in all situations, and use pipeline con
to stop them from running:
- Use the [`interruptible`](../yaml/README.md#interruptible) keyword to stop old pipelines
when they are superceded by a newer pipeline.
when they are superseded by a newer pipeline.
- Use [`rules`](../yaml/README.md#rules) to skip tests that aren't needed. For example,
skip backend tests when only the frontend code is changed.
- Run non-essential [scheduled pipelines](schedules.md) less frequently.

View File

@ -377,16 +377,10 @@ NOTE:
Use merging to customize and override included CI/CD configurations with local
definitions. Local definitions in `.gitlab-ci.yml` override included definitions.
#### Variables with `include`
#### Variables with `include` **(CORE ONLY)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/284883) in GitLab 13.8.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-includepredefined-project-variables). **(CORE ONLY)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/294294) in GitLab 13.9.
You can [use some predefined variables in `include` sections](../variables/where_variables_can_be_used.md#gitlab-ciyml-file)
in your `.gitlab-ci.yml`:
@ -400,25 +394,6 @@ include:
For an example of how you can include these predefined variables, and their impact on CI jobs,
see the following [CI variable demo](https://youtu.be/4XR8gw3Pkos).
##### Enable or disable include:predefined-project-variables **(CORE ONLY)**
Use of predefined project variables in `include` section of `.gitlab-ci.yml` is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:variables_in_include_section_ci)
```
To disable it:
```ruby
Feature.disable(:variables_in_include_section_ci)
```
#### `include:local`
`include:local` includes a file from the same repository as `.gitlab-ci.yml`.
@ -3097,6 +3072,32 @@ job:
- path/*xyz/*
```
#### `artifacts:public`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49775) in GitLab 13.8
> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
`artifacts:public` is used to determine whether the job artifacts should be
publicly available.
The default for `artifacts:public` is `true` which means that the artifacts in
public pipelines are available for download by anonymous and guest users:
```yaml
artifacts:
public: true
```
To deny read access for anonymous and guest users to artifacts in public
pipelines, set `artifacts:public` to `false`:
```yaml
artifacts:
public: false
```
#### `artifacts:exclude`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15122) in GitLab 13.1
@ -3453,7 +3454,7 @@ job1:
The coverage is shown in the UI if at least one line in the job output matches the regular expression.
If there is more than one matched line in the job output, the last line is used.
For the matched line, the first occurence of `\d+(\.\d+)?` is the code coverage.
For the matched line, the first occurrence of `\d+(\.\d+)?` is the code coverage.
Leading zeros are removed.
Coverage output from [child pipelines](../parent_child_pipelines.md) is not recorded

View File

@ -107,7 +107,7 @@ with [domain expertise](#domain-experts).
be **approved by a [frontend foundations member](https://about.gitlab.com/direction/create/ecosystem/frontend-ux-foundations/)**.
- If the license used by the new library hasn't been approved for use in
GitLab, the license must be **approved by a [legal department member](https://about.gitlab.com/handbook/legal/)**.
More information about license compatiblity can be found in our
More information about license compatibility can be found in our
[GitLab Licensing and Compatibility documentation](licensing.md).
1. If your merge request includes adding a new UI/UX paradigm (*1*), it must be
**approved by a [UX lead](https://about.gitlab.com/company/team/)**.

View File

@ -337,7 +337,7 @@ cluster.routing.allocation.disk.watermark.low: 15gb
cluster.routing.allocation.disk.watermark.high: 10gb
```
Restart Elasticsearch, and the `read_only_allow_delete` will clear on it's own.
Restart Elasticsearch, and the `read_only_allow_delete` will clear on its own.
_from "Disk-based Shard Allocation | Elasticsearch Reference" [5.6](https://www.elastic.co/guide/en/elasticsearch/reference/5.6/disk-allocator.html#disk-allocator) and [6.x](https://www.elastic.co/guide/en/elasticsearch/reference/6.7/disk-allocator.html)_
@ -351,7 +351,7 @@ simply reindex everything from scratch.
If your Elasticsearch index is incredibly large it may be too time consuming or
cause too much downtime to reindex from scratch. There aren't any built in
mechanisms for automatically finding discrepencies and resyncing an
mechanisms for automatically finding discrepancies and resyncing an
Elasticsearch index if it gets out of sync but one tool that may be useful is
looking at the logs for all the updates that occurred in a time range you
believe may have been missed. This information is very low level and only

View File

@ -46,7 +46,7 @@ all of the arguments that `in_batches` supports. You should always use
One should proceed with extra caution, and possibly avoid iterating over a column that can contain duplicate values.
When you iterate over an attribute that is not unique, even with the applied max batch size, there is no guarantee that the resulting batches will not surpass it.
The following snippet demonstrates this situation, whe one attempt to select `Ci::Build` entries for users with `id` between `1` and `10,s000`, database returns `1 215 178`
The following snippet demonstrates this situation, when one attempt to select `Ci::Build` entries for users with `id` between `1` and `10,s000`, database returns `1 215 178`
matching rows
```ruby

View File

@ -227,5 +227,5 @@ The following table lists these variables along with their default values.
GitLab may decide to change these settings in order to speed up application performance, lower memory requirements, or both.
You can see how each of these settings affect GC performance, memory use and application start-up time for an idle instance of
GitLab by runnning the `scripts/perf/gc/collect_gc_stats.rb` script. It will output GC stats and general timing data to standard
GitLab by running the `scripts/perf/gc/collect_gc_stats.rb` script. It will output GC stats and general timing data to standard
out as CSV.

View File

@ -180,7 +180,7 @@ For other regular expressions, here are a few guidelines:
- If there's a clean non-regex solution, such as `String#start_with?`, consider using it
- Ruby supports some advanced regex features like [atomic groups](https://www.regular-expressions.info/atomic.html)
and [possessive quantifiers](https://www.regular-expressions.info/possessive.html) that eleminate backtracking
and [possessive quantifiers](https://www.regular-expressions.info/possessive.html) that eliminate backtracking
- Avoid nested quantifiers if possible (for example `(a+)+`)
- Try to be as precise as possible in your regex and avoid the `.` if there's an alternative
- For example, Use `_[^_]+_` instead of `_.*_` to match `_text here_`

View File

@ -31,7 +31,7 @@ Jest tests can be found in `/spec/frontend` and `/ee/spec/frontend` in EE.
## Karma test suite
While GitLab has switched over to [Jest](https://jestjs.io), Karma tests still exist in our
application because some of our specs require a browser and can't be easiliy migrated to Jest.
application because some of our specs require a browser and can't be easily migrated to Jest.
Those specs intend to eventually drop Karma in favor of either Jest or RSpec. You can track this migration
in the [related epic](https://gitlab.com/groups/gitlab-org/-/epics/4900).

View File

@ -17,7 +17,7 @@ You should also reference the [OmniAuth documentation](omniauth.md) for general
|------|-------------|
| Identity Provider (IdP) | The service which manages your user identities such as ADFS, Okta, Onelogin, or Ping Identity. |
| Service Provider (SP) | SAML considers GitLab to be a service provider. |
| Assertion | A piece of information about a user's identity, such as their name or role. Also know as claims or attributes. |
| Assertion | A piece of information about a user's identity, such as their name or role. Also known as claims or attributes. |
| SSO | Single Sign-On. |
| Assertion consumer service URL | The callback on GitLab where users will be redirected after successfully authenticating with the identity provider. |
| Issuer | How GitLab identifies itself to the identity provider. Also known as a "Relying party trust identifier". |

View File

@ -1,6 +1,6 @@
---
stage: Growth
group: Product Analytics
group: Product Intelligence
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---

View File

@ -86,7 +86,7 @@ Feature.enable(:admin_new_user_signups_cap)
## Soft email confirmation
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/47003) in GitLab 12.2.
> - It's [deployed behind a feature flag](../../..//user/feature_flags.md), disabled by default.
> - It's [deployed behind a feature flag](../../../user/feature_flags.md), disabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-soft-email-confirmation).

View File

@ -110,7 +110,8 @@ When [configuring your identify provider](#configuring-your-identity-provider),
### Azure setup notes
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For a demo of the Azure SAML setup including SCIM, see [SCIM Provisioning on Azure Using SAML SSO for Groups Demo](https://youtu.be/24-ZxmTeEBU). Please note that the video is outdated in regards to objectID mapping and the [SCIM documentation should be followed](scim_setup.md#azure-configuration-steps).
For a demo of the Azure SAML setup including SCIM, see [SCIM Provisioning on Azure Using SAML SSO for Groups Demo](https://youtu.be/24-ZxmTeEBU). Please note that the video is outdated in regard to
objectID mapping and the [SCIM documentation should be followed](scim_setup.md#azure-configuration-steps).
| GitLab Setting | Azure Field |
|--------------|----------------|
@ -343,7 +344,7 @@ access.
|------|-------------|
| Identity Provider | The service which manages your user identities such as ADFS, Okta, Onelogin, or Ping Identity. |
| Service Provider | SAML considers GitLab to be a service provider. |
| Assertion | A piece of information about a user's identity, such as their name or role. Also know as claims or attributes. |
| Assertion | A piece of information about a user's identity, such as their name or role. Also known as claims or attributes. |
| SSO | Single Sign On. |
| Assertion consumer service URL | The callback on GitLab where users are redirected after successfully authenticating with the identity provider. |
| Issuer | How GitLab identifies itself to the identity provider. Also known as a "Relying party trust identifier". |

View File

@ -41,7 +41,7 @@ You can see issues with their due dates in the [issues list](index.md#issues-lis
Overdue issues have their icon and date colored red.
To sort issues by their due dates, select **Due date** from the dropdown menu on the right.
Issues are then sorted from the earliest due date to the latest.
To display isses with the latest due dates at the top, select **Sort direction** (**{sort-lowest}**).
To display issues with the latest due dates at the top, select **Sort direction** (**{sort-lowest}**).
Due dates also appear in your [to-do list](../../todos.md).

View File

@ -99,8 +99,6 @@ module Gitlab
end
def expand_variables(data)
return data unless ::Feature.enabled?(:variables_in_include_section_ci)
if data.is_a?(String)
expand(data)
else

View File

@ -5269,6 +5269,9 @@ msgstr ""
msgid "ChangeTypeAction|Cherry-pick"
msgstr ""
msgid "ChangeTypeAction|Pick into branch"
msgstr ""
msgid "ChangeTypeAction|Revert"
msgstr ""
@ -25171,9 +25174,6 @@ msgstr ""
msgid "SecurityReports|Fuzzing artifacts"
msgstr ""
msgid "SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports"
msgstr ""
msgid "SecurityReports|Hide dismissed"
msgstr ""
@ -25237,9 +25237,6 @@ msgstr ""
msgid "SecurityReports|Security scans have run"
msgstr ""
msgid "SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports"
msgstr ""
msgid "SecurityReports|Select a project to add by using the project search field above."
msgstr ""
@ -27752,10 +27749,10 @@ msgstr[1] ""
msgid "Terraform|%{user} updated %{timeAgo}"
msgstr ""
msgid "Terraform|A Terraform report failed to generate."
msgid "Terraform|A report failed to generate."
msgstr ""
msgid "Terraform|A Terraform report was generated in your pipelines."
msgid "Terraform|A report was generated in your pipelines."
msgstr ""
msgid "Terraform|Actions"
@ -27815,10 +27812,10 @@ msgstr ""
msgid "Terraform|States"
msgstr ""
msgid "Terraform|The Terraform report %{name} failed to generate."
msgid "Terraform|The report %{name} failed to generate."
msgstr ""
msgid "Terraform|The Terraform report %{name} was generated in your pipelines."
msgid "Terraform|The report %{name} was generated in your pipelines."
msgstr ""
msgid "Terraform|To remove the State file and its versions, type %{name} to confirm:"
@ -32552,6 +32549,9 @@ msgstr ""
msgid "You have insufficient permissions to create an on-call schedule for this project"
msgstr ""
msgid "You have insufficient permissions to remove an on-call rotation from this project"
msgstr ""
msgid "You have insufficient permissions to remove an on-call schedule from this project"
msgstr ""

View File

@ -45,7 +45,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.179.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "25.12.2",
"@gitlab/ui": "26.0.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-4",
"@rails/ujs": "^6.0.3-4",

View File

@ -2,108 +2,126 @@
require 'spec_helper'
RSpec.describe 'Cherry-pick Commits' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') }
RSpec.describe 'Cherry-pick Commits', :js do
let_it_be(:user) { create(:user) }
let_it_be(:sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
let!(:project) { create_default(:project, :repository, namespace: user.namespace) }
let(:master_pickable_commit) { project.commit(sha) }
before do
sign_in(user)
project.add_maintainer(user)
visit project_commit_path(project, master_pickable_commit.id)
end
context "I cherry-pick a commit" do
it do
find("a[href='#modal-cherry-pick-commit']").click
expect(page).not_to have_content('v1.0.0') # Only branches, not tags
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
click_button 'Cherry-pick'
end
expect(page).to have_content('The commit has been successfully cherry-picked into master.')
end
end
context "I cherry-pick a merge commit" do
it do
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
click_button 'Cherry-pick'
end
expect(page).to have_content('The commit has been successfully cherry-picked into master.')
end
end
context "I cherry-pick a commit that was previously cherry-picked" do
it do
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
click_button 'Cherry-pick'
end
context 'when clicking cherry-pick from the dropdown for a commit on pipelines tab' do
it 'launches the modal form' do
create(:ci_empty_pipeline, sha: sha)
visit project_commit_path(project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
click_button 'Cherry-pick'
click_link 'Pipelines'
open_modal
page.within(modal_selector) do
expect(page).to have_content('Cherry-pick this commit')
end
expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
end
end
context "I cherry-pick a commit in a new merge request", :js do
it do
find('.header-action-buttons a.dropdown-toggle').click
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
click_button 'Cherry-pick'
context 'when starting from the commit tab' do
before do
visit project_commit_path(project, master_pickable_commit.id)
end
context 'when cherry-picking a commit' do
specify do
cherry_pick_commit
expect(page).to have_content('The commit has been successfully cherry-picked into master.')
end
end
wait_for_requests
context 'when cherry-picking a merge commit' do
specify do
cherry_pick_commit
expect(page).to have_content("The commit has been successfully cherry-picked into cherry-pick-#{master_pickable_commit.short_id}. You can now submit a merge request to get this change into the original branch.")
expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master")
expect(page).to have_content('The commit has been successfully cherry-picked into master.')
end
end
context 'when cherry-picking a commit that was previously cherry-picked' do
specify do
cherry_pick_commit
visit project_commit_path(project, master_pickable_commit.id)
cherry_pick_commit
expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
end
end
context 'when cherry-picking a commit in a new merge request' do
specify do
cherry_pick_commit(create_merge_request: true)
expect(page).to have_content("The commit has been successfully cherry-picked into cherry-pick-#{master_pickable_commit.short_id}. You can now submit a merge request to get this change into the original branch.")
expect(page).to have_content("From cherry-pick-#{master_pickable_commit.short_id} into master")
end
end
context 'when I cherry-picking a commit from a different branch' do
specify do
open_modal
page.within(modal_selector) do
click_button 'master'
end
page.within("#{modal_selector} .dropdown-menu") do
find('[data-testid="dropdown-search-box"]').set('feature')
wait_for_requests
click_button 'feature'
end
submit_cherry_pick
expect(page).to have_content('The commit has been successfully cherry-picked into feature.')
end
end
context 'when the project is archived' do
let(:project) { create(:project, :repository, :archived, namespace: user.namespace) }
it 'does not show the cherry-pick link' do
open_dropdown
expect(page).not_to have_text("Cherry-pick")
end
end
end
context "I cherry-pick a commit from a different branch", :js do
it do
find('.header-action-buttons a.dropdown-toggle').click
find(:css, "a[href='#modal-cherry-pick-commit']").click
def cherry_pick_commit(create_merge_request: false)
open_modal
page.within('#modal-cherry-pick-commit') do
click_button 'master'
end
submit_cherry_pick(create_merge_request: create_merge_request)
end
wait_for_requests
def open_dropdown
find('.header-action-buttons .dropdown').click
end
page.within('#modal-cherry-pick-commit .dropdown-menu') do
find('.dropdown-input input').set('feature')
wait_for_requests
click_link "feature"
end
def open_modal
open_dropdown
find('[data-testid="cherry-pick-commit-link"]').click
end
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
click_button 'Cherry-pick'
end
expect(page).to have_content('The commit has been successfully cherry-picked into feature.')
def submit_cherry_pick(create_merge_request: false)
page.within(modal_selector) do
uncheck('create_merge_request') unless create_merge_request
click_button('Cherry-pick')
end
end
context 'when the project is archived' do
let(:project) { create(:project, :repository, :archived, namespace: group) }
it 'does not show the cherry-pick link' do
find('.header-action-buttons a.dropdown-toggle').click
expect(page).not_to have_text("Cherry-pick")
expect(page).not_to have_css("a[href='#modal-cherry-pick-commit']")
end
def modal_selector
'[data-testid="modal-commit"]'
end
end

View File

@ -6,58 +6,89 @@ RSpec.describe 'User reverts a commit', :js do
include RepoHelpers
let_it_be(:user) { create(:user) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let!(:project) { create_default(:project, :repository, namespace: user.namespace) }
before do
sign_in(user)
end
visit(project_commit_path(project, sample_commit.id))
context 'when clicking revert from the dropdown for a commit on pipelines tab' do
it 'launches the modal and is able to submit the revert' do
sha = '7d3b0f7cff5f37573aea97cebfd5692ea1689924'
create(:ci_empty_pipeline, sha: sha)
visit project_commit_path(project, project.commit(sha).id)
click_link 'Pipelines'
open_modal
page.within(modal_selector) do
expect(page).to have_content('Revert this commit')
end
end
end
context 'when starting from the commit tab' do
before do
visit project_commit_path(project, sample_commit.id)
end
context 'without creating a new merge request' do
it 'reverts a commit' do
revert_commit
expect(page).to have_content('The commit has been successfully reverted.')
end
it 'does not revert a previously reverted commit' do
revert_commit
# Visit the comment again once it was reverted.
visit project_commit_path(project, sample_commit.id)
revert_commit
expect(page).to have_content('Sorry, we cannot revert this commit automatically.')
end
end
context 'with creating a new merge request' do
it 'reverts a commit' do
revert_commit(create_merge_request: true)
expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
end
end
context 'when the project is archived' do
let(:project) { create(:project, :repository, :archived, namespace: user.namespace) }
it 'does not show the revert link' do
open_dropdown
expect(page).not_to have_link('Revert')
end
end
end
def revert_commit(create_merge_request: false)
find('.header-action-buttons .dropdown').click
find('[data-testid="revert-commit-link"]').click
open_modal
page.within('[data-testid="modal-commit"]') do
page.within(modal_selector) do
uncheck('create_merge_request') unless create_merge_request
click_button('Revert')
end
end
context 'without creating a new merge request' do
it 'reverts a commit' do
revert_commit
expect(page).to have_content('The commit has been successfully reverted.')
end
it 'does not revert a previously reverted commit' do
revert_commit
# Visit the comment again once it was reverted.
visit project_commit_path(project, sample_commit.id)
revert_commit
expect(page).to have_content('Sorry, we cannot revert this commit automatically.')
end
def open_dropdown
find('.header-action-buttons .dropdown').click
end
context 'with creating a new merge request' do
it 'reverts a commit' do
revert_commit(create_merge_request: true)
expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
end
def open_modal
open_dropdown
find('[data-testid="revert-commit-link"]').click
end
context 'when the project is archived' do
let(:project) { create(:project, :repository, :archived, namespace: user.namespace) }
it 'does not show the revert link' do
find('.header-action-buttons .dropdown').click
expect(page).not_to have_link('Revert')
end
def modal_selector
'[data-testid="modal-commit"]'
end
end

View File

@ -15,6 +15,7 @@ RSpec.describe 'Pipeline', :js do
sign_in(user)
project.add_role(user, role)
stub_feature_flags(graphql_pipeline_details: false)
stub_feature_flags(graphql_pipeline_details_users: false)
end
shared_context 'pipeline builds' do

View File

@ -13,6 +13,7 @@ RSpec.describe 'Pipelines', :js do
before do
sign_in(user)
stub_feature_flags(graphql_pipeline_details: false)
stub_feature_flags(graphql_pipeline_details_users: false)
project.add_developer(user)
project.update!(auto_devops_attributes: { enabled: false })
end

View File

@ -17,6 +17,12 @@ const testCases = [
class: 'status-box-open',
icon: 'issue-open-m',
},
{
name: 'Open',
state: 'locked',
class: 'status-box-open',
icon: 'issue-open-m',
},
{
name: 'Closed',
state: 'closed',

View File

@ -27,8 +27,12 @@ describe('Single Stat Chart component', () => {
describe('computed', () => {
describe('statValue', () => {
it('should interpolate the value and unit props', () => {
expect(findChart().props('value')).toBe('1.00MB');
it('should display the correct value', () => {
expect(findChart().props('value')).toBe('1.00');
});
it('should display the correct value unit', () => {
expect(findChart().props('unit')).toBe('MB');
});
it('should change the value representation to a percentile one', () => {
@ -36,7 +40,8 @@ describe('Single Stat Chart component', () => {
graphData: singleStatGraphData({ max_value: 120 }, { value: 91 }),
});
expect(findChart().props('value')).toContain('75.83%');
expect(findChart().props('value')).toBe('75.83');
expect(findChart().props('unit')).toBe('%');
});
it('should display NaN for non numeric maxValue values', () => {

View File

@ -33,7 +33,7 @@ describe('TerraformPlan', () => {
it('diplays the header text with a name', () => {
expect(wrapper.text()).toContain(
`The Terraform report ${validPlanWithName.job_name} was generated in your pipelines.`,
`The report ${validPlanWithName.job_name} was generated in your pipelines.`,
);
});
@ -55,7 +55,7 @@ describe('TerraformPlan', () => {
});
it('diplays the header text without a name', () => {
expect(wrapper.text()).toContain('A Terraform report was generated in your pipelines.');
expect(wrapper.text()).toContain('A report was generated in your pipelines.');
});
});
@ -70,7 +70,7 @@ describe('TerraformPlan', () => {
it('diplays the header text with a name', () => {
expect(wrapper.text()).toContain(
`The Terraform report ${invalidPlanWithName.job_name} failed to generate.`,
`The report ${invalidPlanWithName.job_name} failed to generate.`,
);
});
@ -85,7 +85,7 @@ describe('TerraformPlan', () => {
});
it('diplays the header text without a name', () => {
expect(wrapper.text()).toContain('A Terraform report failed to generate.');
expect(wrapper.text()).toContain('A report failed to generate.');
});
it('does not render button because url is missing', () => {

View File

@ -1,7 +1,9 @@
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import Api from '~/api';
import createMockApollo from 'helpers/mock_apollo_helper';
import { securityReportDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import axios from '~/lib/utils/axios_utils';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
@ -12,11 +14,14 @@ import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from './mock_data';
import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
jest.mock('~/smart_interval');
jest.mock('~/lib/utils/favicon');
Vue.use(VueApollo);
describe('MrWidgetOptions', () => {
let wrapper;
let mock;
@ -41,7 +46,7 @@ describe('MrWidgetOptions', () => {
gon.features = {};
});
const createComponent = (mrData = mockData) => {
const createComponent = (mrData = mockData, options = {}) => {
if (wrapper) {
wrapper.destroy();
}
@ -50,6 +55,7 @@ describe('MrWidgetOptions', () => {
propsData: {
mrData: { ...mrData },
},
...options,
});
return axios.waitForAll();
@ -815,36 +821,37 @@ describe('MrWidgetOptions', () => {
describe('security widget', () => {
describe.each`
context | hasPipeline | reportType | isFlagEnabled | shouldRender
${'security report and flag enabled'} | ${true} | ${'sast'} | ${true} | ${true}
${'security report and flag disabled'} | ${true} | ${'sast'} | ${false} | ${false}
${'no security report and flag enabled'} | ${true} | ${'foo'} | ${true} | ${false}
${'no pipeline and flag enabled'} | ${false} | ${'sast'} | ${true} | ${false}
`('given $context', ({ hasPipeline, reportType, isFlagEnabled, shouldRender }) => {
context | hasPipeline | isFlagEnabled | shouldRender
${'has pipeline and flag enabled'} | ${true} | ${true} | ${true}
${'has pipeline and flag disabled'} | ${true} | ${false} | ${false}
${'no pipeline and flag enabled'} | ${false} | ${true} | ${false}
`('given $context', ({ hasPipeline, isFlagEnabled, shouldRender }) => {
beforeEach(() => {
gon.features.coreSecurityMrWidget = isFlagEnabled;
if (hasPipeline) {
jest.spyOn(Api, 'pipelineJobs').mockResolvedValue({
data: [{ artifacts: [{ file_type: reportType }] }],
});
}
return createComponent({
const mrData = {
...mockData,
...(hasPipeline ? {} : { pipeline: undefined }),
...(hasPipeline ? {} : { pipeline: null }),
};
// Override top-level mocked requests, which always use a fresh copy of
// mockData, which always includes the full pipeline object.
mock.onGet(mockData.merge_request_widget_path).reply(() => [200, mrData]);
mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]);
return createComponent(mrData, {
apolloProvider: createMockApollo([
[
securityReportDownloadPathsQuery,
async () => ({ data: securityReportDownloadPathsQueryResponse }),
],
]),
});
});
if (shouldRender) {
it('renders', () => {
expect(findSecurityMrWidget().exists()).toBe(true);
});
} else {
it('does not render', () => {
expect(findSecurityMrWidget().exists()).toBe(false);
});
}
it(shouldRender ? 'renders' : 'does not render', () => {
expect(findSecurityMrWidget().exists()).toBe(shouldRender);
});
});
});

View File

@ -21,10 +21,10 @@ exports[`gfm_autocomplete/utils labels config shows the title in the menu item 1
exports[`gfm_autocomplete/utils members config shows an avatar character, name, parent name, and count in the menu item for a group 1`] = `
"
<div class=\\"gl-display-flex gl-align-items-center\\">
<div class=\\"gl-avatar gl-avatar-s24 gl-flex-shrink-0 gl-rounded-small
<div class=\\"gl-avatar gl-avatar-s32 gl-flex-shrink-0 gl-rounded-small
gl-display-flex gl-align-items-center gl-justify-content-center\\" aria-hidden=\\"true\\">
G</div>
<div class=\\"gl-font-sm gl-line-height-normal gl-ml-3\\">
<div class=\\"gl-line-height-normal gl-ml-4\\">
<div>1-1s &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt; (2)</div>
<div class=\\"gl-text-gray-700\\">GitLab Support Team</div>
</div>
@ -36,8 +36,8 @@ exports[`gfm_autocomplete/utils members config shows an avatar character, name,
exports[`gfm_autocomplete/utils members config shows the avatar, name and username in the menu item for a user 1`] = `
"
<div class=\\"gl-display-flex gl-align-items-center\\">
<img class=\\"gl-avatar gl-avatar-s24 gl-flex-shrink-0 gl-avatar-circle\\" src=\\"/uploads/-/system/user/avatar/123456/avatar.png\\" alt=\\"\\" />
<div class=\\"gl-font-sm gl-line-height-normal gl-ml-3\\">
<img class=\\"gl-avatar gl-avatar-s32 gl-flex-shrink-0 gl-avatar-circle\\" src=\\"/uploads/-/system/user/avatar/123456/avatar.png\\" alt=\\"\\" />
<div class=\\"gl-line-height-normal gl-ml-4\\">
<div>My Name &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</div>
<div class=\\"gl-text-gray-700\\">@myusername</div>
</div>

View File

@ -322,6 +322,23 @@ export const secretScanningDiffSuccessMock = {
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
export const securityReportDownloadPathsQueryNoArtifactsResponse = {
project: {
mergeRequest: {
headPipeline: {
id: 'gid://gitlab/Ci::Pipeline/176',
jobs: {
nodes: [],
__typename: 'CiJobConnection',
},
__typename: 'Pipeline',
},
__typename: 'MergeRequest',
},
__typename: 'Project',
},
};
export const securityReportDownloadPathsQueryResponse = {
project: {
mergeRequest: {

View File

@ -8,11 +8,11 @@ import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
expectedDownloadDropdownProps,
securityReportDownloadPathsQueryNoArtifactsResponse,
securityReportDownloadPathsQueryResponse,
sastDiffSuccessMock,
secretScanningDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data';
import Api from '~/api';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
@ -60,6 +60,8 @@ describe('Security reports app', () => {
const pendingHandler = () => new Promise(() => {});
const successHandler = () => Promise.resolve({ data: securityReportDownloadPathsQueryResponse });
const successEmptyHandler = () =>
Promise.resolve({ data: securityReportDownloadPathsQueryNoArtifactsResponse });
const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
const createMockApolloProvider = (handler) => {
localVue.use(VueApollo);
@ -69,178 +71,85 @@ describe('Security reports app', () => {
return createMockApollo(requestHandlers);
};
const anyParams = expect.any(Object);
const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpIconComponent = () => wrapper.find(HelpIcon);
const setupMockJobArtifact = (reportType) => {
jest
.spyOn(Api, 'pipelineJobs')
.mockResolvedValue({ data: [{ artifacts: [{ file_type: reportType }] }] });
};
const expectPipelinesTabAnchor = () => {
const mrTabsMock = { tabShown: jest.fn() };
window.mrTabs = mrTabsMock;
findPipelinesTabAnchor().trigger('click');
expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]);
};
afterEach(() => {
wrapper.destroy();
delete window.mrTabs;
});
describe.each([false, true])(
'given the coreSecurityMrWidgetCounts feature flag is %p',
(coreSecurityMrWidgetCounts) => {
const createComponentWithFlag = (options) =>
createComponent(
merge(
{
provide: {
glFeatures: {
coreSecurityMrWidgetCounts,
},
},
},
options,
),
);
describe.each(SecurityReportsApp.reportTypes)('given a report type %p', (reportType) => {
beforeEach(() => {
window.mrTabs = { tabShown: jest.fn() };
setupMockJobArtifact(reportType);
createComponentWithFlag();
return wrapper.vm.$nextTick();
});
it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
expect(Api.pipelineJobs).toHaveBeenCalledWith(
props.projectId,
props.pipelineId,
anyParams,
);
});
it('renders the expected message', () => {
expect(wrapper.text()).toMatchInterpolatedText(
SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance,
);
});
describe('clicking the anchor to the pipelines tab', () => {
it('calls the mrTabs.tabShown global', () => {
expectPipelinesTabAnchor();
});
});
it('renders a help link', () => {
expect(findHelpIconComponent().props()).toEqual({
helpPath: props.securityReportsDocsPath,
discoverProjectSecurityPath: props.discoverProjectSecurityPath,
});
});
describe('given the artifacts query is loading', () => {
beforeEach(() => {
createComponent({
apolloProvider: createMockApolloProvider(pendingHandler),
});
});
describe('given a report type "foo"', () => {
beforeEach(() => {
setupMockJobArtifact('foo');
createComponentWithFlag();
return wrapper.vm.$nextTick();
});
// TODO: Remove this assertion as part of
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
it('initially renders nothing', () => {
expect(wrapper.html()).toBe('');
});
});
it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
expect(Api.pipelineJobs).toHaveBeenCalledWith(
props.projectId,
props.pipelineId,
anyParams,
);
});
it('renders nothing', () => {
expect(wrapper.html()).toBe('');
});
describe('given the artifacts query loads successfully', () => {
beforeEach(() => {
createComponent({
apolloProvider: createMockApolloProvider(successHandler),
});
});
describe('security artifacts on last page of multi-page response', () => {
const numPages = 3;
it('renders the download dropdown', () => {
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
beforeEach(() => {
jest
.spyOn(Api, 'pipelineJobs')
.mockImplementation(async (projectId, pipelineId, { page }) => {
const requestedPage = parseInt(page, 10);
if (requestedPage < numPages) {
return {
// Some jobs with no relevant artifacts
data: [{}, {}],
headers: { 'x-next-page': String(requestedPage + 1) },
};
} else if (requestedPage === numPages) {
return {
data: [{ artifacts: [{ file_type: SecurityReportsApp.reportTypes[0] }] }],
};
}
it('renders the expected message', () => {
expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
});
throw new Error('Test failed due to request of non-existent jobs page');
});
createComponentWithFlag();
return wrapper.vm.$nextTick();
});
it('fetches all pages', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages);
});
it('renders the expected message', () => {
expect(wrapper.text()).toMatchInterpolatedText(
SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance,
);
});
it('renders a help link', () => {
expect(findHelpIconComponent().props()).toEqual({
helpPath: props.securityReportsDocsPath,
discoverProjectSecurityPath: props.discoverProjectSecurityPath,
});
});
});
describe('given an error from the API', () => {
let error;
beforeEach(() => {
error = new Error('an error');
jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error);
createComponentWithFlag();
return wrapper.vm.$nextTick();
});
it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
expect(Api.pipelineJobs).toHaveBeenCalledWith(
props.projectId,
props.pipelineId,
anyParams,
);
});
it('renders nothing', () => {
expect(wrapper.html()).toBe('');
});
it('calls createFlash correctly', () => {
expect(createFlash.mock.calls).toEqual([
[
{
message: SecurityReportsApp.i18n.apiError,
captureError: true,
error,
},
],
]);
});
describe('given the artifacts query loads successfully with no artifacts', () => {
beforeEach(() => {
createComponent({
apolloProvider: createMockApolloProvider(successEmptyHandler),
});
},
);
});
// TODO: Remove this assertion as part of
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
it('initially renders nothing', () => {
expect(wrapper.html()).toBe('');
});
});
describe('given the artifacts query fails', () => {
beforeEach(() => {
createComponent({
apolloProvider: createMockApolloProvider(failureHandler),
});
});
it('calls createFlash correctly', () => {
expect(createFlash).toHaveBeenCalledWith({
message: SecurityReportsApp.i18n.apiError,
captureError: true,
error: expect.any(Error),
});
});
// TODO: Remove this assertion as part of
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
it('renders nothing', () => {
expect(wrapper.html()).toBe('');
});
});
describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => {
let mock;
@ -253,6 +162,7 @@ describe('Security reports app', () => {
coreSecurityMrWidgetCounts: true,
},
},
apolloProvider: createMockApolloProvider(successHandler),
}),
);
@ -274,11 +184,7 @@ describe('Security reports app', () => {
${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE}
`(
'given a $pathProp and $reportType artifact',
({ reportType, pathProp, path, successResponse, successMessage }) => {
beforeEach(() => {
setupMockJobArtifact(reportType);
});
({ pathProp, path, successResponse, successMessage }) => {
describe('when loading', () => {
beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 });
@ -294,11 +200,11 @@ describe('Security reports app', () => {
});
it('should have loading message', () => {
expect(wrapper.text()).toBe('Security scanning is loading');
expect(wrapper.text()).toContain('Security scanning is loading');
});
it('should not render the pipeline tab anchor', () => {
expect(findPipelinesTabAnchor().exists()).toBe(false);
it('renders the download dropdown', () => {
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
@ -319,8 +225,8 @@ describe('Security reports app', () => {
expect(trimText(wrapper.text())).toContain(successMessage);
});
it('should render the pipeline tab anchor', () => {
expectPipelinesTabAnchor();
it('renders the download dropdown', () => {
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
@ -341,125 +247,25 @@ describe('Security reports app', () => {
expect(trimText(wrapper.text())).toContain('Loading resulted in an error');
});
it('should render the pipeline tab anchor', () => {
expectPipelinesTabAnchor();
it('renders the download dropdown', () => {
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
describe('when the comparison endpoint is not provided', () => {
beforeEach(() => {
mock.onGet(path).replyOnce(500);
createComponentWithFlagEnabled();
return waitForPromises();
});
it('renders the basic scansHaveRun message', () => {
expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
});
});
},
);
});
describe('given coreSecurityMrWidgetDownloads feature flag is enabled', () => {
const createComponentWithFlagEnabled = (options) =>
createComponent(
merge(options, {
provide: {
glFeatures: {
coreSecurityMrWidgetDownloads: true,
},
},
}),
);
describe('given the query is loading', () => {
beforeEach(() => {
createComponentWithFlagEnabled({
apolloProvider: createMockApolloProvider(pendingHandler),
});
});
// TODO: Remove this assertion as part of
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
it('initially renders nothing', () => {
expect(wrapper.html()).toBe('');
});
});
describe('given the query loads successfully', () => {
beforeEach(() => {
createComponentWithFlagEnabled({
apolloProvider: createMockApolloProvider(successHandler),
});
});
it('renders the download dropdown', () => {
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
it('renders the expected message', () => {
const text = wrapper.text();
expect(text).not.toContain(SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance);
expect(text).toContain(SecurityReportsApp.i18n.scansHaveRun);
});
it('should not render the pipeline tab anchor', () => {
expect(findPipelinesTabAnchor().exists()).toBe(false);
});
});
describe('given the query fails', () => {
beforeEach(() => {
createComponentWithFlagEnabled({
apolloProvider: createMockApolloProvider(failureHandler),
});
});
it('calls createFlash correctly', () => {
expect(createFlash).toHaveBeenCalledWith({
message: SecurityReportsApp.i18n.apiError,
captureError: true,
error: expect.any(Error),
});
});
// TODO: Remove this assertion as part of
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
it('renders nothing', () => {
expect(wrapper.html()).toBe('');
});
});
});
describe('given coreSecurityMrWidgetCounts and coreSecurityMrWidgetDownloads feature flags are enabled', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(SAST_COMPARISON_PATH).replyOnce(200, sastDiffSuccessMock);
mock.onGet(SECRET_SCANNING_COMPARISON_PATH).replyOnce(200, secretScanningDiffSuccessMock);
createComponent({
propsData: {
sastComparisonPath: SAST_COMPARISON_PATH,
secretScanningComparisonPath: SECRET_SCANNING_COMPARISON_PATH,
},
provide: {
glFeatures: {
coreSecurityMrWidgetCounts: true,
coreSecurityMrWidgetDownloads: true,
},
},
apolloProvider: createMockApolloProvider(successHandler),
});
return waitForPromises();
});
afterEach(() => {
mock.restore();
});
it('renders the download dropdown', () => {
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
it('renders the expected counts message', () => {
expect(trimText(wrapper.text())).toContain(
'Security scanning detected 3 potential vulnerabilities 2 Critical 1 High and 0 Others',
);
});
it('should not render the pipeline tab anchor', () => {
expect(findPipelinesTabAnchor().exists()).toBe(false);
});
});
});

View File

@ -7,20 +7,13 @@ RSpec.describe CommitsHelper do
context 'when current_user exists' do
before do
allow(helper).to receive(:current_user).and_return(double('User'))
allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
end
it 'renders a div for Vue' do
result = helper.revert_commit_link('_commit_', '_path_', pajamas: true)
result = helper.revert_commit_link
expect(result).to include('js-revert-commit-trigger')
end
it 'does not render a div for Vue' do
result = helper.revert_commit_link('_commit_', '_path_')
expect(result).not_to include('js-revert-commit-trigger')
end
end
context 'when current_user does not exist' do
@ -29,7 +22,33 @@ RSpec.describe CommitsHelper do
end
it 'does not render anything' do
result = helper.revert_commit_link(double('Commit'), '_path_')
result = helper.revert_commit_link
expect(result).to be_nil
end
end
end
describe '#cherry_pick_commit_link' do
context 'when current_user exists' do
before do
allow(helper).to receive(:current_user).and_return(double('User'))
end
it 'renders a div for Vue' do
result = helper.cherry_pick_commit_link
expect(result).to include('js-cherry-pick-commit-trigger')
end
end
context 'when current_user does not exist' do
before do
allow(helper).to receive(:current_user).and_return(nil)
end
it 'does not render anything' do
result = helper.cherry_pick_commit_link
expect(result).to be_nil
end

View File

@ -323,20 +323,6 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do
expect { subject }.to raise_error(described_class::AmbigiousSpecificationError)
end
end
context 'when feature flag is turned off' do
let(:values) do
{ include: full_local_file_path }
end
before do
stub_feature_flags(variables_in_include_section_ci: false)
end
it 'does not expand the variables' do
expect(subject[0].location).to eq('$CI_PROJECT_PATH' + local_file)
end
end
end
end
end

View File

@ -56,6 +56,15 @@ RSpec.describe Pages::LookupPath do
include_examples 'uses disk storage'
it 'return nil when legacy storage is disabled and there is no deployment' do
stub_feature_flags(pages_serve_from_legacy_storage: false)
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(described_class::LegacyStorageDisabledError)
.and_call_original
expect(source).to eq(nil)
end
context 'when there is pages deployment' do
let(:deployment) { create(:pages_deployment, project: project) }

View File

@ -26,31 +26,34 @@ RSpec.describe Pages::VirtualDomain do
describe '#lookup_paths' do
let(:project_a) { instance_double(Project) }
let(:project_z) { instance_double(Project) }
let(:pages_lookup_path_a) { instance_double(Pages::LookupPath, prefix: 'aaa') }
let(:pages_lookup_path_z) { instance_double(Pages::LookupPath, prefix: 'zzz') }
let(:project_b) { instance_double(Project) }
let(:project_c) { instance_double(Project) }
let(:pages_lookup_path_a) { instance_double(Pages::LookupPath, prefix: 'aaa', source: { type: 'zip', path: 'https://example.com' }) }
let(:pages_lookup_path_b) { instance_double(Pages::LookupPath, prefix: 'bbb', source: { type: 'zip', path: 'https://example.com' }) }
let(:pages_lookup_path_without_source) { instance_double(Pages::LookupPath, prefix: 'ccc', source: nil) }
context 'when there is pages domain provided' do
let(:domain) { instance_double(PagesDomain) }
subject(:virtual_domain) { described_class.new([project_a, project_z], domain: domain) }
subject(:virtual_domain) { described_class.new([project_a, project_b, project_c], domain: domain) }
it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
expect(project_a).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_a)
expect(project_z).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_z)
expect(project_b).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_b)
expect(project_c).to receive(:pages_lookup_path).with(domain: domain, trim_prefix: nil).and_return(pages_lookup_path_without_source)
expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_b, pages_lookup_path_a])
end
end
context 'when there is trim_prefix provided' do
subject(:virtual_domain) { described_class.new([project_a, project_z], trim_prefix: 'group/') }
subject(:virtual_domain) { described_class.new([project_a, project_b], trim_prefix: 'group/') }
it 'returns collection of projects pages lookup paths sorted by prefix in reverse' do
expect(project_a).to receive(:pages_lookup_path).with(trim_prefix: 'group/', domain: nil).and_return(pages_lookup_path_a)
expect(project_z).to receive(:pages_lookup_path).with(trim_prefix: 'group/', domain: nil).and_return(pages_lookup_path_z)
expect(project_b).to receive(:pages_lookup_path).with(trim_prefix: 'group/', domain: nil).and_return(pages_lookup_path_b)
expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_z, pages_lookup_path_a])
expect(virtual_domain.lookup_paths).to eq([pages_lookup_path_b, pages_lookup_path_a])
end
end
end

View File

@ -82,6 +82,7 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
it 'skips the repository' do
expect(ContainerExpirationPolicies::CleanupService).not_to receive(:new)
expect(worker).to receive(:log_extra_metadata_on_done).with(:container_repository_id, repository.id)
expect(worker).to receive(:log_extra_metadata_on_done).with(:project_id, repository.project.id)
expect(worker).to receive(:log_extra_metadata_on_done).with(:cleanup_status, :skipped)
expect { subject }.to change { ContainerRepository.waiting_for_cleanup.count }.from(1).to(0)
@ -213,8 +214,10 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
end
def expect_log_extra_metadata(service_response:, cleanup_status: :finished, truncated: false)
expect(worker).to receive(:log_extra_metadata_on_done).with(:cleanup_status, cleanup_status)
expect(worker).to receive(:log_extra_metadata_on_done).with(:container_repository_id, repository.id)
expect(worker).to receive(:log_extra_metadata_on_done).with(:project_id, repository.project.id)
expect(worker).to receive(:log_extra_metadata_on_done).with(:cleanup_status, cleanup_status)
%i[cleanup_tags_service_original_size cleanup_tags_service_before_truncate_size cleanup_tags_service_after_truncate_size cleanup_tags_service_before_delete_size].each do |field|
value = service_response.payload[field]
expect(worker).to receive(:log_extra_metadata_on_done).with(field, value) unless value.nil?

View File

@ -876,10 +876,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
"@gitlab/ui@25.12.2":
version "25.12.2"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-25.12.2.tgz#ad47680da4b067140e8d48a04e807352660b9cca"
integrity sha512-y+uks00z+4kivTYu+l2mrjYT3nfnBS+xKWIUQ9xrkZVCC069V+DffPK+jVRzzhQ67hOMP5LVdaUEOcUplgFvGA==
"@gitlab/ui@26.0.0":
version "26.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-26.0.0.tgz#7fa93726478042b1570b2bd3b909217a31177b36"
integrity sha512-X22mc3qVBAkfAZ2DRqbPnJ6upzjWlzGLWmHR4l+3MhOBbMBi4EXIuF19nixC5u8bjCMGkK3wMIiZj3C3HsmQ7A==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"