Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-23 15:08:38 +00:00
parent 3e68d38487
commit 16dbaf57bc
76 changed files with 997 additions and 399 deletions

View file

@ -0,0 +1,98 @@
<!---
Please read this!
This template is for reporting a security vulnerability about GitLab or
GitLab.com
Strongly consider reporting via https://hackerone.com/gitlab, as
HackerOne is our preferred disclosure platform.
See also:
- https://about.gitlab.com/security/disclosure/
- https://about.gitlab.com/handbook/engineering/security/#creating-new-security-issues
- https://about.gitlab.com/handbook/engineering/security/#engaging-the-security-on-call
--->
### Summary
<!-- Summarize the bug encountered concisely. -->
### Steps to reproduce
<!-- Describe how one can reproduce the issue - this is very important. Please use an ordered list. -->
### Example Project
<!-- If possible, please create an example project here on GitLab.com that exhibits the problematic
behavior, and link to it here in the bug report. If you are using an older version of GitLab, this
will also determine whether the bug is fixed in a more recent version. -->
### What is the current *bug* behavior?
<!-- Describe what actually happens. -->
### What is the expected *correct* behavior?
<!-- Describe what you should see instead. -->
### Relevant logs and/or screenshots
<!-- Paste any relevant logs - please use code blocks (```) to format console output, logs, and code
as it's tough to read otherwise. -->
### Output of checks
<!-- If you are reporting a bug on GitLab.com, write: This bug happens on GitLab.com -->
#### Results of GitLab environment info
<!-- Input any relevant GitLab environment information if needed. -->
<details>
<summary>Expand for output related to GitLab environment info</summary>
<pre>
(For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:env:info`)
(For installations from source run and paste the output of:
`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
</pre>
</details>
#### Results of GitLab application Check
<!-- Input any relevant GitLab application check information if needed. -->
<details>
<summary>Expand for output related to the GitLab application check</summary>
<pre>
(For installations with omnibus-gitlab package run and paste the output of:
`sudo gitlab-rake gitlab:check SANITIZE=true`)
(For installations from source run and paste the output of:
`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
(we will only investigate if the tests are passing)
</pre>
</details>
### Possible fixes
<!-- If you can, link to the line of code that might be responsible for the problem. -->
---
<!-- Do not edit past here unless you are certain of the impact -->
cc @gitlab-com/gl-security/appsec
/label ~"type::bug" ~"bug::vulnerability"
/confidential

View file

@ -2,6 +2,16 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 14.9.1 (2022-03-23)
### Fixed (1 change)
- [Fix backups not working when feature_flags table does not exist](gitlab-org/gitlab@4cc3cd6cf6eb256a9837ef92a6fdb4991cd1642c) ([merge request](gitlab-org/gitlab!83388))
### Changed (1 change)
- [Alias user_email_lookup_limit to search_rate_limit](gitlab-org/gitlab@424c277fc4c994df60ea68acb8988537526108e4) ([merge request](gitlab-org/gitlab!83388))
## 14.9.0 (2022-03-21)
### Added (119 changes)

View file

@ -33,10 +33,37 @@ export default class ShortcutsIssuable extends Shortcuts {
Mousetrap.bind(keysFor(ISSUABLE_COMMENT_OR_REPLY), ShortcutsIssuable.replyWithSelectedText);
Mousetrap.bind(keysFor(ISSUABLE_EDIT_DESCRIPTION), ShortcutsIssuable.editIssue);
Mousetrap.bind(keysFor(MR_COPY_SOURCE_BRANCH_NAME), ShortcutsIssuable.copyBranchName);
/**
* We're attaching a global focus event listener on document for
* every markdown input field.
*/
$(document).on(
'focus',
'.js-vue-markdown-field .js-gfm-input',
ShortcutsIssuable.handleMarkdownFieldFocus,
);
}
/**
* This event handler preserves last focused markdown input field.
* @param {Object} event
*/
static handleMarkdownFieldFocus({ currentTarget }) {
ShortcutsIssuable.$lastFocusedReplyField = $(currentTarget);
}
static replyWithSelectedText() {
const $replyField = $('.js-main-target-form .js-vue-comment-form');
let $replyField = $('.js-main-target-form .js-vue-comment-form');
// Ensure that markdown input is still present in the DOM
// otherwise fall back to main comment input field.
if (
ShortcutsIssuable.$lastFocusedReplyField &&
isElementVisible(ShortcutsIssuable.$lastFocusedReplyField?.get(0))
) {
$replyField = ShortcutsIssuable.$lastFocusedReplyField;
}
if (!$replyField.length || $replyField.is(':hidden') /* Other tab selected in MR */) {
return false;

View file

@ -5,6 +5,8 @@ import AwardsList from '~/vue_shared/components/awards_list.vue';
import createstore from './store';
export default (el) => {
if (!el) return null;
const {
dataset: { path },
} = el;

View file

@ -1,6 +1,5 @@
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
@ -22,6 +21,7 @@ import MilestoneSelect from '~/milestones/milestone_select';
import initNotesApp from '~/notes';
import { store } from '~/notes/stores';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
import FilteredSearchServiceDesk from './filtered_search_service_desk';
export function initFilteredSearchServiceDesk() {
@ -72,15 +72,7 @@ export function initShow() {
initRelatedMergeRequests();
initSentryErrorStackTrace();
const awardEmojiEl = document.getElementById('js-vue-awards-block');
if (awardEmojiEl) {
import('~/emoji/awards_app')
.then((m) => m.default(awardEmojiEl))
.catch(() => {});
} else {
loadAwardsHandler();
}
initAwardsApp(document.getElementById('js-vue-awards-block'));
import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then((module) => module.default())

View file

@ -292,40 +292,18 @@ export default {
class="line-resolve-btn note-action-button"
@click="onResolve"
/>
<template v-if="canAwardEmoji">
<emoji-picker
v-if="glFeatures.improvedEmojiPicker"
toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-3 gl-p-0! gl-shadow-none! gl-bg-transparent!"
@click="setAwardEmoji"
>
<template #button-content>
<gl-icon class="link-highlight award-control-icon-neutral gl-m-0!" name="slight-smile" />
<gl-icon class="link-highlight award-control-icon-positive gl-m-0!" name="smiley" />
<gl-icon class="link-highlight award-control-icon-super-positive gl-m-0!" name="smile" />
</template>
</emoji-picker>
<gl-button
v-else
v-gl-tooltip
:class="{ 'js-user-authored': isAuthoredByCurrentUser }"
class="note-action-button note-emoji-button add-reaction-button js-add-award js-note-emoji"
category="tertiary"
variant="default"
:title="$options.i18n.addReactionLabel"
:aria-label="$options.i18n.addReactionLabel"
data-position="right"
>
<span class="reaction-control-icon reaction-control-icon-neutral">
<gl-icon name="slight-smile" />
</span>
<span class="reaction-control-icon reaction-control-icon-positive">
<gl-icon name="smiley" />
</span>
<span class="reaction-control-icon reaction-control-icon-super-positive">
<gl-icon name="smile" />
</span>
</gl-button>
</template>
<emoji-picker
v-if="canAwardEmoji"
toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-3 gl-p-0! gl-shadow-none! gl-bg-transparent!"
data-testid="note-emoji-button"
@click="setAwardEmoji"
>
<template #button-content>
<gl-icon class="link-highlight award-control-icon-neutral gl-m-0!" name="slight-smile" />
<gl-icon class="link-highlight award-control-icon-positive gl-m-0!" name="smiley" />
<gl-icon class="link-highlight award-control-icon-super-positive gl-m-0!" name="smile" />
</template>
</emoji-picker>
<reply-button
v-if="showReply"
ref="replyButton"

View file

@ -1,6 +1,5 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { initPipelineCountListener } from '~/commit/pipelines/utils';
import { initIssuableSidebar } from '~/issuable';
@ -8,23 +7,16 @@ import StatusBox from '~/issuable/components/status_box.vue';
import createDefaultClient from '~/lib/graphql';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
import getStateQuery from './queries/get_state.query.graphql';
export default function initMergeRequestShow() {
const awardEmojiEl = document.getElementById('js-vue-awards-block');
new ZenMode(); // eslint-disable-line no-new
initPipelineCountListener(document.querySelector('#commit-pipeline-table-view'));
new ShortcutsIssuable(true); // eslint-disable-line no-new
initSourcegraph();
initIssuableSidebar();
if (awardEmojiEl) {
import('~/emoji/awards_app')
.then((m) => m.default(awardEmojiEl))
.catch(() => {});
} else {
loadAwardsHandler();
}
initAwardsApp(document.getElementById('js-vue-awards-block'));
const el = document.querySelector('.js-mr-status-box');
const apolloProvider = new VueApollo({

View file

@ -1,9 +1,4 @@
import '~/snippet/snippet_show';
import initAwardsApp from '~/emoji/awards_app';
const awardEmojiEl = document.getElementById('js-vue-awards-block');
if (awardEmojiEl) {
import('~/emoji/awards_app')
.then((m) => m.default(awardEmojiEl))
.catch(() => {});
}
initAwardsApp(document.getElementById('js-vue-awards-block'));

View file

@ -0,0 +1,50 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import { __ } from '~/locale';
import PipelineGraphWrapper from './graph/graph_component_wrapper.vue';
import Dag from './dag/dag.vue';
import JobsApp from './jobs/jobs_app.vue';
import TestReports from './test_reports/test_reports.vue';
export default {
i18n: {
tabs: {
failedJobsTitle: __('Failed Jobs'),
jobsTitle: __('Jobs'),
needsTitle: __('Needs'),
pipelineTitle: __('Pipeline'),
testsTitle: __('Tests'),
},
},
components: {
Dag,
GlTab,
GlTabs,
JobsApp,
FailedJobsApp: JobsApp,
PipelineGraphWrapper,
TestReports,
},
};
</script>
<template>
<gl-tabs>
<gl-tab :title="$options.i18n.tabs.pipelineTitle" data-testid="pipeline-tab">
<pipeline-graph-wrapper />
</gl-tab>
<gl-tab :title="$options.i18n.tabs.needsTitle" data-testid="dag-tab">
<dag />
</gl-tab>
<gl-tab :title="$options.i18n.tabs.jobsTitle" data-testid="jobs-tab">
<jobs-app />
</gl-tab>
<gl-tab :title="$options.i18n.tabs.failedJobsTitle" data-testid="failed-jobs-tab">
<failed-jobs-app />
</gl-tab>
<gl-tab :title="$options.i18n.tabs.testsTitle" data-testid="tests-tab">
<test-reports />
</gl-tab>
<slot></slot>
</gl-tabs>
</template>

View file

@ -13,6 +13,7 @@ const SELECTORS = {
PIPELINE_GRAPH: '#js-pipeline-graph-vue',
PIPELINE_HEADER: '#js-pipeline-header-vue',
PIPELINE_NOTIFICATION: '#js-pipeline-notification',
PIPELINE_TABS: '#js-pipeline-tabs',
PIPELINE_TESTS: '#js-pipeline-tests-detail',
PIPELINE_JOBS: '#js-pipeline-jobs-vue',
};
@ -28,22 +29,6 @@ export default async function initPipelineDetailsBundle() {
});
}
try {
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
} catch {
createFlash({
message: __('An error occurred while loading the pipeline.'),
});
}
try {
createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
} catch {
createFlash({
message: __('An error occurred while loading a section of this page.'),
});
}
try {
createPipelineNotificationApp(SELECTORS.PIPELINE_NOTIFICATION, apolloProvider);
} catch {
@ -52,27 +37,47 @@ export default async function initPipelineDetailsBundle() {
});
}
try {
createDagApp(apolloProvider);
} catch {
createFlash({
message: __('An error occurred while loading the Needs tab.'),
});
}
if (gon.features?.pipelineTabsVue) {
const { createPipelineTabs } = await import('./pipeline_tabs');
try {
createTestDetails(SELECTORS.PIPELINE_TESTS);
} catch {
createFlash({
message: __('An error occurred while loading the Test Reports tab.'),
});
}
try {
createPipelineTabs(SELECTORS.PIPELINE_TABS, apolloProvider);
} catch {
createFlash({
message: __('An error occurred while loading a section of this page.'),
});
}
} else {
try {
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
} catch {
createFlash({
message: __('An error occurred while loading the pipeline.'),
});
}
try {
createPipelineJobsApp(SELECTORS.PIPELINE_JOBS);
} catch {
createFlash({
message: __('An error occurred while loading the Jobs tab.'),
});
try {
createDagApp(apolloProvider);
} catch {
createFlash({
message: __('An error occurred while loading the Needs tab.'),
});
}
try {
createTestDetails(SELECTORS.PIPELINE_TESTS);
} catch {
createFlash({
message: __('An error occurred while loading the Test Reports tab.'),
});
}
try {
createPipelineJobsApp(SELECTORS.PIPELINE_JOBS);
} catch {
createFlash({
message: __('An error occurred while loading the Jobs tab.'),
});
}
}
}

View file

@ -0,0 +1,44 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import PipelineTabs from 'ee_else_ce/pipelines/components/pipeline_tabs.vue';
import { reportToSentry } from './utils';
Vue.use(VueApollo);
const createPipelineTabs = (selector, apolloProvider) => {
const el = document.querySelector(selector);
if (!el) return;
const { dataset } = document.querySelector(selector);
const {
canGenerateCodequalityReports,
codequalityReportDownloadPath,
downloadablePathForReportType,
exposeSecurityDashboard,
exposeLicenseScanningData,
} = dataset;
// eslint-disable-next-line no-new
new Vue({
el: selector,
components: {
PipelineTabs,
},
apolloProvider,
provide: {
canGenerateCodequalityReports: JSON.parse(canGenerateCodequalityReports),
codequalityReportDownloadPath,
downloadablePathForReportType,
exposeSecurityDashboard: JSON.parse(exposeSecurityDashboard),
exposeLicenseScanningData: JSON.parse(exposeLicenseScanningData),
},
errorCaptured(err, _vm, info) {
reportToSentry('pipeline_tabs', `error: ${err}, info: ${info}`);
},
render(createElement) {
return createElement(PipelineTabs);
},
});
};
export { createPipelineTabs };

View file

@ -1,20 +0,0 @@
import { AwardsHandler } from '~/awards_handler';
class EmojiMenuInModal extends AwardsHandler {
constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback, targetContainerEl) {
super(emoji);
this.selectEmojiCallback = selectEmojiCallback;
this.toggleButtonSelector = toggleButtonSelector;
this.menuClass = menuClass;
this.targetContainerEl = targetContainerEl;
this.bindEvents();
}
postEmoji($emojiButton, awardUrl, selectedEmoji) {
this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji));
}
}
export default EmojiMenuInModal;

View file

@ -19,10 +19,8 @@ import { __, s__, sprintf } from '~/locale';
import { updateUserStatus } from '~/rest_api';
import { timeRanges } from '~/vue_shared/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import EmojiMenuInModal from './emoji_menu_in_modal';
import { isUserBusy } from './utils';
const emojiMenuClass = 'js-modal-status-emoji-menu';
export const AVAILABILITY_STATUS = {
BUSY: 'busy',
NOT_SET: 'not_set',
@ -83,7 +81,6 @@ export default {
emoji: this.currentEmoji,
emojiMenu: null,
emojiTag: '',
isEmojiMenuVisible: false,
message: this.currentMessage,
modalId: 'set-user-status-modal',
noEmoji: true,
@ -105,17 +102,11 @@ export default {
mounted() {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
beforeDestroy() {
if (this.emojiMenu) {
this.emojiMenu.destroy();
}
},
methods: {
closeModal() {
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
setupEmojiListAndAutocomplete() {
const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu';
const emojiAutocomplete = new GfmAutoComplete();
emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true });
@ -127,16 +118,6 @@ export default {
this.noEmoji = this.emoji === '';
this.defaultEmojiTag = Emoji.glEmojiTag(this.defaultEmoji);
if (!this.glFeatures.improvedEmojiPicker) {
this.emojiMenu = new EmojiMenuInModal(
Emoji,
toggleEmojiMenuButtonSelector,
emojiMenuClass,
this.setEmoji,
this.$refs.userStatusForm,
);
}
this.setDefaultEmoji();
})
.catch(() =>
@ -145,19 +126,6 @@ export default {
}),
);
},
showEmojiMenu(e) {
e.stopPropagation();
this.isEmojiMenuVisible = true;
this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton));
},
hideEmojiMenu() {
if (!this.isEmojiMenuVisible) {
return;
}
this.isEmojiMenuVisible = false;
this.emojiMenu.hideMenuElement($(`.${emojiMenuClass}`));
},
setDefaultEmoji() {
const { emojiTag } = this;
const hasStatusMessage = Boolean(this.message.length);
@ -173,16 +141,12 @@ export default {
this.clearEmoji();
}
},
setEmoji(emoji, emojiTag) {
setEmoji(emoji) {
this.emoji = emoji;
this.noEmoji = false;
this.clearEmoji();
if (this.glFeatures.improvedEmojiPicker) {
this.emojiTag = Emoji.glEmojiTag(this.emoji);
} else {
this.emojiTag = emojiTag;
}
this.emojiTag = Emoji.glEmojiTag(this.emoji);
},
clearEmoji() {
if (this.emojiTag) {
@ -194,7 +158,6 @@ export default {
this.message = '';
this.noEmoji = true;
this.clearEmoji();
this.hideEmojiMenu();
},
removeStatus() {
this.availability = false;
@ -249,7 +212,6 @@ export default {
:action-secondary="$options.actionSecondary"
modal-class="set-user-status-modal"
@shown="setupEmojiListAndAutocomplete"
@hide="hideEmojiMenu"
@primary="setStatus"
@secondary="removeStatus"
>
@ -264,7 +226,6 @@ export default {
<div class="input-group gl-mb-5">
<span class="input-group-prepend">
<emoji-picker
v-if="glFeatures.improvedEmojiPicker"
dropdown-class="gl-h-full"
toggle-class="btn emoji-menu-toggle-button gl-px-4! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
boundary="viewport"
@ -283,27 +244,6 @@ export default {
</span>
</template>
</emoji-picker>
<button
v-else
ref="toggleEmojiMenuButton"
v-gl-tooltip.bottom.hover
:title="s__('SetStatusModal|Add status emoji')"
:aria-label="s__('SetStatusModal|Add status emoji')"
name="button"
type="button"
class="js-toggle-emoji-menu emoji-menu-toggle-button btn"
@click="showEmojiMenu"
>
<span v-safe-html:[$options.safeHtmlConfig]="emojiTag"></span>
<span
v-show="noEmoji"
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
>
<gl-icon name="slight-smile" class="award-control-icon-neutral" />
<gl-icon name="smiley" class="award-control-icon-positive" />
<gl-icon name="smile" class="award-control-icon-super-positive" />
</span>
</button>
</span>
<input
ref="statusMessageField"
@ -314,7 +254,6 @@ export default {
name="user[status][message]"
@keyup="setDefaultEmoji"
@keyup.enter.prevent
@click="hideEmojiMenu"
/>
<span v-show="isDirty" class="input-group-append">
<button

View file

@ -198,10 +198,10 @@ export default {
</gl-button>
<div v-if="canAwardEmoji" class="award-menu-holder gl-my-2">
<emoji-picker
v-if="glFeatures.improvedEmojiPicker"
v-gl-tooltip.viewport
:title="__('Add reaction')"
:toggle-class="['add-reaction-button btn-icon gl-relative!', { 'is-active': isMenuOpen }]"
data-testid="emoji-picker"
@click="handleAward"
@shown="setIsMenuOpen(true)"
@hidden="setIsMenuOpen(false)"
@ -219,24 +219,6 @@ export default {
</span>
</template>
</emoji-picker>
<gl-button
v-else
v-gl-tooltip.viewport
:class="addButtonClass"
class="add-reaction-button js-add-award"
title="Add reaction"
:aria-label="__('Add reaction')"
>
<span class="reaction-control-icon reaction-control-icon-neutral">
<gl-icon name="slight-smile" />
</span>
<span class="reaction-control-icon reaction-control-icon-positive">
<gl-icon name="smiley" />
</span>
<span class="reaction-control-icon reaction-control-icon-super-positive">
<gl-icon name="smile" />
</span>
</gl-button>
</div>
</div>
</template>

View file

@ -37,7 +37,6 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_download_code!, only: [:related_branches]
before_action do
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
push_frontend_feature_flag(:vue_issues_list, project&.group, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
push_frontend_feature_flag(:contacts_autocomplete, project&.group, default_enabled: :yaml)
@ -48,7 +47,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:confidential_notes, project&.group, default_enabled: :yaml)
push_frontend_feature_flag(:issue_assignees_widget, project, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_issue_discussions, project, default_enabled: :yaml)
push_frontend_feature_flag(:work_items, project&.group, default_enabled: :yaml)
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]

View file

@ -37,7 +37,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
push_frontend_feature_flag(:paginated_notes, project, default_enabled: :yaml)
push_frontend_feature_flag(:confidential_notes, project, default_enabled: :yaml)
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_mr_widgets_extensions, project, default_enabled: :yaml)
push_frontend_feature_flag(:rebase_without_ci_ui, project, default_enabled: :yaml)

View file

@ -17,6 +17,10 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
before_action do
push_frontend_feature_flag(:pipeline_tabs_vue, @project, default_enabled: :yaml)
end
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }

View file

@ -14,10 +14,6 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController
before_action :authorize_read_snippet!, except: [:new, :index]
before_action :authorize_update_snippet!, only: :edit
before_action only: [:show] do
push_frontend_feature_flag(:improved_emoji_picker, @project, default_enabled: :yaml)
end
urgency :low, [:index]
def index

View file

@ -2,12 +2,12 @@
class Projects::WorkItemsController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:work_items, project, default_enabled: :yaml)
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
end
feature_category :not_owned
def index
render_404 unless Feature.enabled?(:work_items, project, default_enabled: :yaml)
render_404 unless project&.work_items_feature_flag_enabled?
end
end

View file

@ -41,7 +41,7 @@ class ProjectsController < Projects::ApplicationController
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
push_frontend_feature_flag(:work_items, @project, default_enabled: :yaml)
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
end
layout :determine_layout

View file

@ -33,7 +33,7 @@ module Mutations
def resolve(project_path:, **attributes)
project = authorized_find!(project_path)
unless Feature.enabled?(:work_items, project, default_enabled: :yaml)
unless project.work_items_feature_flag_enabled?
return { errors: ['`work_items` feature flag disabled for this project'] }
end

View file

@ -31,7 +31,7 @@ module Mutations
def resolve(id:, work_item_data:)
work_item = authorized_find!(id: id)
unless Feature.enabled?(:work_items, work_item.project, default_enabled: :yaml)
unless work_item.project.work_items_feature_flag_enabled?
return { errors: ['`work_items` feature flag disabled for this project'] }
end

View file

@ -20,7 +20,7 @@ module Mutations
def resolve(id:)
work_item = authorized_find!(id: id)
unless Feature.enabled?(:work_items, work_item.project, default_enabled: :yaml)
unless work_item.project.work_items_feature_flag_enabled?
return { errors: ['`work_items` feature flag disabled for this project'] }
end

View file

@ -28,7 +28,7 @@ module Mutations
def resolve(id:, **attributes)
work_item = authorized_find!(id: id)
unless Feature.enabled?(:work_items, work_item.project, default_enabled: :yaml)
unless work_item.project.work_items_feature_flag_enabled?
return { errors: ['`work_items` feature flag disabled for this project'] }
end

View file

@ -12,7 +12,7 @@ module Resolvers
def resolve(id:)
work_item = authorized_find!(id: id)
return unless Feature.enabled?(:work_items, work_item.project, default_enabled: :yaml)
return unless work_item.project.work_items_feature_flag_enabled?
work_item
end

View file

@ -11,7 +11,7 @@ module Resolvers
' Argument is experimental and can be removed in the future without notice.'
def resolve(taskable: nil)
return unless Feature.enabled?(:work_items, object, default_enabled: :yaml)
return unless feature_flag_enabled_for_parent?(object)
# This will require a finder in the future when groups/projects get their work item types
# All groups/projects use the default types for now
@ -20,6 +20,14 @@ module Resolvers
base_scope.order_by_name_asc
end
private
def feature_flag_enabled_for_parent?(parent)
return false unless parent.is_a?(::Project) || parent.is_a?(::Group)
parent.work_items_feature_flag_enabled?
end
end
end
end

View file

@ -251,9 +251,7 @@ module IssuesHelper
end
def award_emoji_issue_api_path(issue)
if Feature.enabled?(:improved_emoji_picker, issue.project, default_enabled: :yaml)
api_v4_projects_issues_award_emoji_path(id: issue.project.id, issue_iid: issue.iid)
end
api_v4_projects_issues_award_emoji_path(id: issue.project.id, issue_iid: issue.iid)
end
end

View file

@ -203,9 +203,7 @@ module MergeRequestsHelper
end
def award_emoji_merge_request_api_path(merge_request)
if Feature.enabled?(:improved_emoji_picker, merge_request.project, default_enabled: :yaml)
api_v4_projects_merge_requests_award_emoji_path(id: merge_request.project.id, merge_request_iid: merge_request.iid)
end
api_v4_projects_merge_requests_award_emoji_path(id: merge_request.project.id, merge_request_iid: merge_request.iid)
end
private

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
module Projects
module PipelineHelper
def js_pipeline_tabs_data(project, pipeline)
{
can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json,
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
pipeline_project_path: project.full_path
}
end
end
end
Projects::PipelineHelper.prepend_mod

View file

@ -77,8 +77,6 @@ module SnippetsHelper
end
def project_snippets_award_api_path(snippet)
if Feature.enabled?(:improved_emoji_picker, snippet.project, default_enabled: :yaml)
api_v4_projects_snippets_award_emoji_path(id: snippet.project.id, snippet_id: snippet.id)
end
api_v4_projects_snippets_award_emoji_path(id: snippet.project.id, snippet_id: snippet.id)
end
end

View file

@ -16,10 +16,13 @@ class BulkImport < ApplicationRecord
enum source_type: { gitlab: 0 }
scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) }
state_machine :status, initial: :created do
state :created, value: 0
state :started, value: 1
state :finished, value: 2
state :timeout, value: 3
state :failed, value: -1
event :start do
@ -30,6 +33,11 @@ class BulkImport < ApplicationRecord
transition started: :finished
end
event :cleanup_stale do
transition created: :timeout
transition started: :timeout
end
event :fail_op do
transition any => :failed
end

View file

@ -51,11 +51,13 @@ class BulkImports::Entity < ApplicationRecord
enum source_type: { group_entity: 0, project_entity: 1 }
scope :by_user_id, ->(user_id) { joins(:bulk_import).where(bulk_imports: { user_id: user_id }) }
scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) }
state_machine :status, initial: :created do
state :created, value: 0
state :started, value: 1
state :finished, value: 2
state :timeout, value: 3
state :failed, value: -1
event :start do
@ -70,6 +72,11 @@ class BulkImports::Entity < ApplicationRecord
event :fail_op do
transition any => :failed
end
event :cleanup_stale do
transition created: :timeout
transition started: :timeout
end
end
def self.all_human_statuses

View file

@ -14,6 +14,7 @@ class ContainerRepository < ApplicationRecord
ACTIVE_MIGRATION_STATES = %w[pre_importing importing].freeze
MIGRATION_STATES = (IDLE_MIGRATION_STATES + ACTIVE_MIGRATION_STATES).freeze
ABORTABLE_MIGRATION_STATES = (ACTIVE_MIGRATION_STATES + %w[pre_import_done default]).freeze
SKIPPABLE_MIGRATION_STATES = (ABORTABLE_MIGRATION_STATES + %w[import_aborted]).freeze
IRRECONCILABLE_MIGRATIONS_STATUSES = %w[import_in_progress pre_import_in_progress pre_import_canceled import_canceled].freeze
@ -35,7 +36,7 @@ class ContainerRepository < ApplicationRecord
enum status: { delete_scheduled: 0, delete_failed: 1 }
enum expiration_policy_cleanup_status: { cleanup_unscheduled: 0, cleanup_scheduled: 1, cleanup_unfinished: 2, cleanup_ongoing: 3 }
enum migration_skipped_reason: { not_in_plan: 0, too_many_retries: 1, too_many_tags: 2, root_namespace_in_deny_list: 3, migration_canceled: 4 }
enum migration_skipped_reason: { not_in_plan: 0, too_many_retries: 1, too_many_tags: 2, root_namespace_in_deny_list: 3, migration_canceled: 4, not_found: 5 }
delegate :client, :gitlab_api_client, to: :registry
@ -137,7 +138,7 @@ class ContainerRepository < ApplicationRecord
end
event :skip_import do
transition ABORTABLE_MIGRATION_STATES.map(&:to_sym) => :import_skipped
transition SKIPPABLE_MIGRATION_STATES.map(&:to_sym) => :import_skipped
end
event :retry_pre_import do
@ -184,6 +185,12 @@ class ContainerRepository < ApplicationRecord
container_repository.migration_retries_count += 1
end
after_transition any => :import_aborted do |container_repository|
if container_repository.retried_too_many_times?
container_repository.skip_import(reason: :too_many_retries)
end
end
before_transition import_aborted: any do |container_repository|
container_repository.migration_aborted_at = nil
container_repository.migration_aborted_in_state = nil
@ -310,9 +317,16 @@ class ContainerRepository < ApplicationRecord
try_count = 0
begin
try_count += 1
return true if yield == :ok
abort_import
case yield
when :ok
return true
when :not_found
skip_import(reason: :not_found)
else
abort_import
end
false
rescue TooManyImportsError
if try_count <= ::ContainerRegistry::Migration.start_max_retries
@ -325,6 +339,10 @@ class ContainerRepository < ApplicationRecord
end
end
def retried_too_many_times?
migration_retries_count >= ContainerRegistry::Migration.max_retries
end
def last_import_step_done_at
[migration_pre_import_done_at, migration_import_done_at, migration_aborted_at].compact.max
end

View file

@ -815,6 +815,15 @@ class Group < Namespace
].compact.min
end
def work_items_feature_flag_enabled?
actors = [root_ancestor]
actors << self if root_ancestor != self
actors.any? do |actor|
Feature.enabled?(:work_items, actor, default_enabled: :yaml)
end
end
private
def max_member_access(user_ids)

View file

@ -2832,6 +2832,10 @@ class Project < ApplicationRecord
pending_delete? || hidden?
end
def work_items_feature_flag_enabled?
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self, default_enabled: :yaml)
end
private
# overridden in EE

View file

@ -27,6 +27,8 @@
= s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
#js-pipeline-notification{ data: { deprecated_keywords_doc_path: help_page_path('ci/yaml/index.md', anchor: 'deprecated-keywords'), full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
- if Feature.enabled?(:pipeline_tabs_vue, @project, default_enabled: :yaml)
#js-pipeline-tabs{ data: js_pipeline_tabs_data(@project, @pipeline) }
- else
= render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
.js-pipeline-details-vue{ data: { metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline) } }

View file

@ -192,6 +192,15 @@
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:bulk_imports_stuck_import
:worker_name: BulkImports::StuckImportWorker
:feature_category: :importers
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:ci_archive_traces_cron
:worker_name: Ci::ArchiveTracesCronWorker
:feature_category: :continuous_integration

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
module BulkImports
class StuckImportWorker
include ApplicationWorker
# This worker does not schedule other workers that require context.
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
idempotent!
data_consistency :always
feature_category :importers
def perform
BulkImport.stale.find_each do |import|
import.cleanup_stale
end
BulkImports::Entity.stale.find_each do |import|
import.cleanup_stale
end
end
end
end

View file

@ -1,8 +0,0 @@
---
name: improved_emoji_picker
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54469
rollout_issue_url:
milestone: '13.9'
type: development
group: group::code review
default_enabled: true

View file

@ -1,8 +1,8 @@
---
name: ci_skip_legacy_extra_minutes_recalculation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78476
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341730
milestone: '14.8'
name: pipeline_tabs_vue
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80401
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353118
milestone: '14.10'
type: development
group: group::pipeline execution
group: group::pipeline authoring
default_enabled: false

View file

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324351
milestone: '13.11'
type: development
group: group::source code
default_enabled: false
default_enabled: true

View file

@ -500,6 +500,9 @@ Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsW
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *'
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker'
Settings.cron_jobs['bulk_imports_stuck_import_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['bulk_imports_stuck_import_worker']['cron'] ||= '0 */4 * * *'
Settings.cron_jobs['bulk_imports_stuck_import_worker']['job_class'] = 'BulkImports::StuckImportWorker'
Settings.cron_jobs['import_stuck_project_import_jobs'] ||= Settingslogic.new({})
Settings.cron_jobs['import_stuck_project_import_jobs']['cron'] ||= '15 * * * *'
Settings.cron_jobs['import_stuck_project_import_jobs']['job_class'] = 'Gitlab::Import::StuckProjectImportJobsWorker'

View file

@ -14,12 +14,12 @@
- Requests that have the `status` field set to `approved`.
Beginning in GitLab 15.0, status checks will only be updated to a passing state if the `status` field is both present
and set to `pass`. Requests that:
and set to `passed`. Requests that:
- Do not contain the `status` field will be rejected with a `422` error. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/338827).
- Contain any value other than `pass` will cause the status check to fail. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/339039).
- Contain any value other than `passed` will cause the status check to fail. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/339039).
To align with this change, API calls to list external status checks will also return the value of `pass` rather than
To align with this change, API calls to list external status checks will also return the value of `passed` rather than
`approved` for status checks that have passed.
# The following items are not published on the docs page, but may be used in the future.
stage: "Manage"

View file

@ -766,6 +766,8 @@ This example for Omnibus GitLab assumes you're using the following mailbox: `inc
##### Configure Microsoft Graph
> Alternative Azure deployments [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5978) in GitLab 14.9.
```ruby
gitlab_rails['incoming_email_enabled'] = true
@ -788,4 +790,19 @@ gitlab_rails['incoming_email_inbox_options'] = {
}
```
For Microsoft Cloud for US Government or [other Azure deployments](https://docs.microsoft.com/en-us/graph/deployments), configure the `azure_ad_endpoint` and `graph_endpoint` settings.
- Example for Microsoft Cloud for US Government:
```ruby
gitlab_rails['incoming_email_inbox_options'] = {
'azure_ad_endpoint': 'https://login.microsoftonline.us',
'graph_endpoint': 'https://graph.microsoft.us',
'tenant_id': '<YOUR-TENANT-ID>',
'client_id': '<YOUR-CLIENT-ID>',
'client_secret': '<YOUR-CLIENT-SECRET>',
'poll_interval': 60 # Optional
}
```
The Microsoft Graph API is not yet supported in source installations. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/326169) for more details.

View file

@ -95,6 +95,12 @@ To remove a metric:
used to test the [`UsageDataController#create`](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/3760ef28/spec/controllers/usage_data_controller_spec.rb#L75)
endpoint, and assure that test suite does not fail when metric that you wish to remove is not included into test payload.
1. Remove data from Redis
For [Ordinary Redis](implement.md#ordinary-redis-counters) counters remove data stored in Redis.
- Add a migration to remove the data from Redis for the related Redis keys. For more details, see [this MR example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82604/diffs).
1. Create an issue in the
[GitLab Data Team project](https://gitlab.com/gitlab-data/analytics/-/issues).
Ask for confirmation that the metric is not referred to in any SiSense dashboards and

View file

@ -313,12 +313,12 @@ Specifically, the following are deprecated:
- Requests that have the `status` field set to `approved`.
Beginning in GitLab 15.0, status checks will only be updated to a passing state if the `status` field is both present
and set to `pass`. Requests that:
and set to `passed`. Requests that:
- Do not contain the `status` field will be rejected with a `422` error. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/338827).
- Contain any value other than `pass` will cause the status check to fail. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/339039).
- Contain any value other than `passed` will cause the status check to fail. For more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/339039).
To align with this change, API calls to list external status checks will also return the value of `pass` rather than
To align with this change, API calls to list external status checks will also return the value of `passed` rather than
`approved` for status checks that have passed.
**Planned removal milestone: 15.0 (2022-05-22)**

View file

@ -138,9 +138,9 @@ migrated:
In a [rails console session](../../../administration/operations/rails_console.md#starting-a-rails-console-session),
you can find the failure or error messages for the group import attempt using:
```shell
```ruby
# Get relevant import records
import = BulkImports::Entity.where(namespace_id: Group.id).bulk_import
import = BulkImports::Entity.where(namespace_id: Group.id).map(&:bulk_import)
# Alternative lookup by user
import = BulkImport.where(user_id: User.find(...)).last
@ -154,3 +154,18 @@ entities.map(&:failures).flatten
# Alternative failure lookup by status
entities.where(status: [-1]).pluck(:destination_name, :destination_namespace, :status)
```
### Stale imports
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/352985) in GitLab 14.10.
When troubleshooting group migration, an import may not complete because the import workers took
longer than 8 hours to execute. In this case, the `status` of either a `BulkImport` or
`BulkImport::Entity` is `3` (`timeout`):
```ruby
# Get relevant import records
import = BulkImports::Entity.where(namespace_id: Group.id).map(&:bulk_import)
import.status #=> 3 means that the import timed out.
```

View file

@ -35,7 +35,7 @@ Projects include the following [features](https://about.gitlab.com/features/):
- [Deploy tokens](deploy_tokens/index.md): Manage access to the repository and Container Registry.
- [Web IDE](web_ide/index.md)
- [CVE ID Requests](../application_security/cve_id_request.md): Request a CVE identifier to track a
vulnerability in your project. **(FREE SAAS)**
vulnerability in your project.
**Issues and merge requests:**
@ -83,7 +83,7 @@ Projects include the following [features](https://about.gitlab.com/features/):
- [Kubernetes cluster integration](../infrastructure/clusters/index.md): Connect your GitLab project
with a Kubernetes cluster.
- [Feature Flags](../../operations/feature_flags.md): Ship different features
by dynamically toggling functionality. **(PREMIUM)**
by dynamically toggling functionality.
- [GitLab Pages](pages/index.md): Build, test, and deploy your static
website.
@ -92,8 +92,8 @@ Projects include the following [features](https://about.gitlab.com/features/):
- [Wiki](wiki/index.md): Document your GitLab project in an integrated Wiki.
- [Snippets](../snippets.md): Store, share and collaborate on code snippets.
- [Value Stream Analytics](../analytics/value_stream_analytics.md): Review your development lifecycle.
- [Insights](insights/index.md): Configure the insights that matter for your projects. **(ULTIMATE)**
- [Security Dashboard](../application_security/security_dashboard/index.md) **(ULTIMATE)**
- [Insights](insights/index.md): Configure the insights that matter for your projects.
- [Security Dashboard](../application_security/security_dashboard/index.md)
- [Syntax highlighting](highlighting.md): Customize
your code blocks, overriding the default language choice.
- [Badges](badges.md): Add an image to the **Project information** page.
@ -102,9 +102,9 @@ Projects include the following [features](https://about.gitlab.com/features/):
associated with a released version of your code.
- [Package Registry](../packages/package_registry/index.md): Publish and install packages.
- [Code owners](code_owners.md): Specify code owners for specific files.
- [License Compliance](../compliance/license_compliance/index.md): Approve and deny licenses for projects. **(ULTIMATE)**
- [Dependency List](../application_security/dependency_list/index.md): View project dependencies. **(ULTIMATE)**
- [Requirements](requirements/index.md): Create criteria to check your products against. **(ULTIMATE)**
- [License Compliance](../compliance/license_compliance/index.md): Approve and deny licenses for projects.
- [Dependency List](../application_security/dependency_list/index.md): View project dependencies.
- [Requirements](requirements/index.md): Create criteria to check your products against.
- [Code Intelligence](code_intelligence.md): Navigate code.
## Project integrations

View file

@ -241,7 +241,8 @@ The configuration options are the same as for configuring
##### Microsoft Graph
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214900) in GitLab 13.11.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214900) in GitLab 13.11.
> - Alternative Azure deployments [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5978) in GitLab 14.9.
Service Desk can be configured to read Microsoft Exchange Online mailboxes with the Microsoft
Graph API instead of IMAP. Follow the [documentation in the incoming email section for setting up an OAuth2 application for Microsoft Graph](../../administration/incoming_email.md#microsoft-graph).
@ -263,6 +264,22 @@ Graph API instead of IMAP. Follow the [documentation in the incoming email secti
}
```
For Microsoft Cloud for US Government or [other Azure deployments](https://docs.microsoft.com/en-us/graph/deployments), configure the `azure_ad_endpoint` and `graph_endpoint` settings.
- Example for Microsoft Cloud for US Government:
```ruby
gitlab_rails['service_desk_email_inbox_options'] = {
'azure_ad_endpoint': 'https://login.microsoftonline.us',
'graph_endpoint': 'https://graph.microsoft.us',
'tenant_id': '<YOUR-TENANT-ID>',
'client_id': '<YOUR-CLIENT-ID>',
'client_secret': '<YOUR-CLIENT-SECRET>',
'poll_interval': 60 # Optional
}
}
```
The Microsoft Graph API is not yet supported in source installations. See [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/326169) for more details.
#### Configuring a custom email address suffix

View file

@ -76,13 +76,13 @@ To create a project in GitLab:
- [custom template](#create-a-project-from-a-custom-template).
- [HIPAA audit protocol template](#create-a-project-from-the-hipaa-audit-protocol-template).
- [Import a project](../../user/project/import/index.md)
from a different repository. Contact your GitLab administrator if this option is not available.
from a different repository. Contact your GitLab administrator if this option is not available.
- [Connect an external repository to GitLab CI/CD](../../ci/ci_cd_for_external_repos/index.md).
- For a list of words that you cannot use as project names, see
[reserved project and group names](../../user/reserved_names.md).
[reserved project and group names](../../user/reserved_names.md).
- For a list of characters that you cannot use in project and group names, see
[limitations on project and group names](../../user/reserved_names.md#limitations-on-project-and-group-names).
[limitations on project and group names](../../user/reserved_names.md#limitations-on-project-and-group-names).
## Create a blank project
@ -100,11 +100,11 @@ To create a blank project:
- In the **Project target (optional)** field, select your project's deployment target.
This information helps GitLab better understand its users and their deployment requirements.
- To modify the project's [viewing and access rights](../public_access.md) for
users, change the **Visibility Level**.
users, change the **Visibility Level**.
- To create README file so that the Git repository is initialized, has a default branch, and
can be cloned, select **Initialize repository with a README**.
- To analyze the source code in the project for known security vulnerabilities,
select **Enable Static Application Security Testing (SAST)**.
select **Enable Static Application Security Testing (SAST)**.
1. Select **Create project**.
## Create a project from a built-in template
@ -133,12 +133,12 @@ To create a project from a built-in template:
then change the slug.
- In the **Project description (optional)** field, enter the description of your project's dashboard.
- To modify the project's [viewing and access rights](../public_access.md) for users,
change the **Visibility Level**.
change the **Visibility Level**.
1. Select **Create project**.
## Create a project from a custom template **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6860) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.2.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6860) in GitLab 11.2.
Custom project templates are available at:
@ -159,7 +159,7 @@ Custom project templates are available at:
then change the slug.
- The description of your project's dashboard in the **Project description (optional)** field.
- To modify the project's [viewing and access rights](../public_access.md) for users,
change the **Visibility Level**.
change the **Visibility Level**.
1. Select **Create project**.
## Create a project from the HIPAA Audit Protocol template **(ULTIMATE)**
@ -185,7 +185,7 @@ To create a project from the HIPAA Audit Protocol template:
then change the slug.
- In the **Project description (optional)** field, enter the description of your project's dashboard.
- To modify the project's [viewing and access rights](../public_access.md) for users,
change the **Visibility Level**.
change the **Visibility Level**.
1. Select **Create project**.
## Create a new project with Git push
@ -207,7 +207,7 @@ used or renamed project, use the [UI](#create-a-project) or the [Projects API](.
Prerequisites:
- To push with SSH, you must have [an SSH key](../../ssh/index.md) that is
[added to your GitLab account](../../ssh/index.md#add-an-ssh-key-to-your-gitlab-account).
[added to your GitLab account](../../ssh/index.md#add-an-ssh-key-to-your-gitlab-account).
- You must have permission to add new projects to a namespace. To check if you have permission:
1. On the top bar, select **Menu > Projects**.
@ -409,9 +409,9 @@ To disable fetching:
1. Disable checksum queries in `GONOSUMDB`.
- If the module name or its prefix is in `GOPRIVATE` or `GONOPROXY`, Go does not query module
proxies.
proxies.
- If the module name or its prefix is in `GONOPRIVATE` or `GONOSUMDB`, Go does not query
Checksum databases.
Checksum databases.
### Fetch Go modules from Geo secondary sites

View file

@ -146,7 +146,7 @@ module API
use :pagination
end
get ':id/pipelines/:pipeline_id/bridges', feature_category: :pipeline_authoring do
get ':id/pipelines/:pipeline_id/bridges', urgency: :low, feature_category: :pipeline_authoring do
authorize!(:read_build, user_project)
pipeline = user_project.all_pipelines.find(params[:pipeline_id])

View file

@ -23,7 +23,7 @@ module API
params do
use :pagination
end
get ':id/variables' do
get ':id/variables', urgency: :low do
variables = user_project.variables
present paginate(variables), with: Entities::Ci::Variable
end

View file

@ -21,7 +21,7 @@ module API
optional :include_merged_yaml, type: Boolean, desc: 'Whether or not to include merged CI config yaml in the response'
optional :include_jobs, type: Boolean, desc: 'Whether or not to include CI jobs in the response'
end
post '/lint' do
post '/lint', urgency: :low do
unauthorized! unless can_lint_ci?
result = Gitlab::Ci::Lint.new(project: nil, current_user: current_user)

View file

@ -53,7 +53,6 @@ module Gitlab
# made globally available to the frontend
push_frontend_feature_flag(:usage_data_api, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:security_auto_fix, default_enabled: false)
push_frontend_feature_flag(:improved_emoji_picker, default_enabled: :yaml)
push_frontend_feature_flag(:new_header_search, default_enabled: :yaml)
push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml)
push_frontend_feature_flag(:sandboxed_mermaid, default_enabled: :yaml)
@ -73,6 +72,15 @@ module Gitlab
push_to_gon_attributes(:features, name, enabled)
end
# Exposes the state of a feature flag to the frontend code.
# Can be used for more complex feature flag checks.
#
# name - The name of the feature flag, e.g. `my_feature`.
# enabled - Boolean to be pushed directly to the frontend. Should be fetched by checking a feature flag.
def push_force_frontend_feature_flag(name, enabled)
push_to_gon_attributes(:features, name, !!enabled)
end
def push_to_gon_attributes(key, name, enabled)
var_name = name.to_s.camelize(:lower)
# Here the `true` argument signals gon that the value should be merged

View file

@ -34269,9 +34269,6 @@ msgstr ""
msgid "Set what should be replicated by this secondary site."
msgstr ""
msgid "SetStatusModal|Add status emoji"
msgstr ""
msgid "SetStatusModal|An indicator appears next to your name and avatar"
msgstr ""

View file

@ -10,6 +10,7 @@ RSpec.describe 'Commits' do
before do
sign_in(user)
stub_ci_pipeline_to_return_yaml_file
stub_feature_flags(pipeline_tabs_vue: false)
end
let(:creator) { create(:user, developer_projects: [project]) }
@ -93,6 +94,7 @@ RSpec.describe 'Commits' do
context 'Download artifacts', :js do
before do
stub_feature_flags(pipeline_tabs_vue: false)
create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
end
@ -122,6 +124,7 @@ RSpec.describe 'Commits' do
context "when logged as reporter", :js do
before do
stub_feature_flags(pipeline_tabs_vue: false)
project.add_reporter(user)
create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
visit builds_project_pipeline_path(project, pipeline)

View file

@ -15,12 +15,20 @@ RSpec.describe 'Blob shortcuts', :js do
end
shared_examples "quotes the selected text" do
it "quotes the selected text", :quarantine do
select_element('.note-text')
it 'quotes the selected text in main comment form', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/356388' do
select_element('#notes-list .note:first-child .note-text')
find('body').native.send_key('r')
expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text)
end
it 'quotes the selected text in the discussion reply form', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/356388' do
find('#notes-list .note:first-child .js-reply-button').click
select_element('#notes-list .note:first-child .note-text')
find('body').native.send_key('r')
expect(find('#notes-list .note:first-child .js-vue-markdown-field .js-gfm-input').value).to include(note_text)
end
end
describe 'pressing "r"' do

View file

@ -8,8 +8,6 @@ RSpec.describe 'User edit profile' do
let(:user) { create(:user) }
before do
stub_feature_flags(improved_emoji_picker: false)
sign_in(user)
visit(profile_path)
end
@ -169,10 +167,9 @@ RSpec.describe 'User edit profile' do
context 'user status', :js do
def select_emoji(emoji_name, is_modal = false)
emoji_menu_class = is_modal ? '.js-modal-status-emoji-menu' : '.js-status-emoji-menu'
toggle_button = find('.js-toggle-emoji-menu')
toggle_button = find('.emoji-menu-toggle-button')
toggle_button.click
emoji_button = find(%Q{#{emoji_menu_class} .js-emoji-btn gl-emoji[data-name="#{emoji_name}"]})
emoji_button = find("gl-emoji[data-name=\"#{emoji_name}\"]")
emoji_button.click
end
@ -207,7 +204,7 @@ RSpec.describe 'User edit profile' do
end
it 'adds message and emoji to user status' do
emoji = 'tanabata_tree'
emoji = '8ball'
message = 'Playing outside'
select_emoji(emoji)
fill_in 'js-status-message-field', with: message
@ -356,7 +353,7 @@ RSpec.describe 'User edit profile' do
end
it 'adds emoji to user status' do
emoji = 'biohazard'
emoji = '8ball'
open_user_status_modal
select_emoji(emoji, true)
set_user_status_in_modal
@ -387,18 +384,18 @@ RSpec.describe 'User edit profile' do
it 'opens the emoji modal again after closing it' do
open_user_status_modal
select_emoji('biohazard', true)
select_emoji('8ball', true)
find('.js-toggle-emoji-menu').click
find('.emoji-menu-toggle-button').click
expect(page).to have_selector('.emoji-menu')
expect(page).to have_selector('.emoji-picker-emoji')
end
it 'does not update the awards panel emoji' do
project.add_maintainer(user)
visit(project_issue_path(project, issue))
emoji = 'biohazard'
emoji = '8ball'
open_user_status_modal
select_emoji(emoji, true)
@ -420,7 +417,7 @@ RSpec.describe 'User edit profile' do
end
it 'adds message and emoji to user status' do
emoji = 'tanabata_tree'
emoji = '8ball'
message = 'Playing outside'
open_user_status_modal
select_emoji(emoji, true)
@ -495,9 +492,7 @@ RSpec.describe 'User edit profile' do
open_user_status_modal
find('.js-status-message-field').native.send_keys(message)
within('.js-toggle-emoji-menu') do
expect(page).to have_emoji('speech_balloon')
end
expect(page).to have_emoji('speech_balloon')
end
context 'note header' do

View file

@ -15,6 +15,7 @@ RSpec.describe 'Pipeline', :js do
before do
sign_in(user)
project.add_role(user, role)
stub_feature_flags(pipeline_tabs_vue: false)
end
shared_context 'pipeline builds' do
@ -356,6 +357,7 @@ RSpec.describe 'Pipeline', :js do
context 'page tabs' do
before do
stub_feature_flags(pipeline_tabs_vue: false)
visit_pipeline
end
@ -388,6 +390,7 @@ RSpec.describe 'Pipeline', :js do
let(:pipeline) { create(:ci_pipeline, :with_test_reports, :with_report_results, project: project) }
before do
stub_feature_flags(pipeline_tabs_vue: false)
visit_pipeline
wait_for_requests
end
@ -924,6 +927,7 @@ RSpec.describe 'Pipeline', :js do
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
before do
stub_feature_flags(pipeline_tabs_vue: false)
visit builds_project_pipeline_path(project, pipeline)
end
@ -944,6 +948,10 @@ RSpec.describe 'Pipeline', :js do
end
context 'page tabs' do
before do
stub_feature_flags(pipeline_tabs_vue: false)
end
it 'shows Pipeline, Jobs and DAG tabs with link' do
expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs')
@ -1014,6 +1022,10 @@ RSpec.describe 'Pipeline', :js do
end
describe 'GET /:project/-/pipelines/:id/failures' do
before do
stub_feature_flags(pipeline_tabs_vue: false)
end
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: '1234') }
let(:pipeline_failures_page) { failures_project_pipeline_path(project, pipeline) }
let!(:failed_build) { create(:ci_build, :failed, pipeline: pipeline) }
@ -1139,6 +1151,7 @@ RSpec.describe 'Pipeline', :js do
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
before do
stub_feature_flags(pipeline_tabs_vue: false)
visit dag_project_pipeline_path(project, pipeline)
end

View file

@ -623,6 +623,7 @@ RSpec.describe 'Pipelines', :js do
create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3, ref: 'master')
stub_feature_flags(pipeline_tabs_vue: false)
visit project_pipeline_path(project, pipeline)
wait_for_requests
end

View file

@ -87,8 +87,7 @@ describe('noteActions', () => {
});
it('should render emoji link', () => {
expect(wrapper.find('.js-add-award').exists()).toBe(true);
expect(wrapper.find('.js-add-award').attributes('data-position')).toBe('right');
expect(wrapper.find('[data-testid="note-emoji-button"]').exists()).toBe(true);
});
describe('actions dropdown', () => {

View file

@ -0,0 +1,61 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineTabs from '~/pipelines/components/pipeline_tabs.vue';
import PipelineGraphWrapper from '~/pipelines/components/graph/graph_component_wrapper.vue';
import Dag from '~/pipelines/components/dag/dag.vue';
import JobsApp from '~/pipelines/components/jobs/jobs_app.vue';
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
describe('The Pipeline Tabs', () => {
let wrapper;
const findDagTab = () => wrapper.findByTestId('dag-tab');
const findFailedJobsTab = () => wrapper.findByTestId('failed-jobs-tab');
const findJobsTab = () => wrapper.findByTestId('jobs-tab');
const findPipelineTab = () => wrapper.findByTestId('pipeline-tab');
const findTestsTab = () => wrapper.findByTestId('tests-tab');
const findDagApp = () => wrapper.findComponent(Dag);
const findFailedJobsApp = () => wrapper.findComponent(JobsApp);
const findJobsApp = () => wrapper.findComponent(JobsApp);
const findPipelineApp = () => wrapper.findComponent(PipelineGraphWrapper);
const findTestsApp = () => wrapper.findComponent(TestReports);
const createComponent = (propsData = {}) => {
wrapper = extendedWrapper(
shallowMount(PipelineTabs, {
propsData,
stubs: {
Dag: { template: '<div id="dag"/>' },
JobsApp: { template: '<div class="jobs" />' },
PipelineGraph: { template: '<div id="graph" />' },
TestReports: { template: '<div id="tests" />' },
},
}),
);
};
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
});
// The failed jobs MUST be removed from here and tested individually once
// the logic for the tab is implemented.
describe('Tabs', () => {
it.each`
tabName | tabComponent | appComponent
${'Pipeline'} | ${findPipelineTab} | ${findPipelineApp}
${'Dag'} | ${findDagTab} | ${findDagApp}
${'Jobs'} | ${findJobsTab} | ${findJobsApp}
${'Failed Jobs'} | ${findFailedJobsTab} | ${findFailedJobsApp}
${'Tests'} | ${findTestsTab} | ${findTestsApp}
`('shows $tabName tab and its associated component', ({ appComponent, tabComponent }) => {
expect(tabComponent().exists()).toBe(true);
expect(appComponent().exists()).toBe(true);
});
});
});

View file

@ -26,7 +26,7 @@ describe('SetStatusModalWrapper', () => {
defaultEmoji,
};
const createComponent = (props = {}, improvedEmojiPicker = false) => {
const createComponent = (props = {}) => {
return shallowMount(SetStatusModalWrapper, {
propsData: {
...defaultProps,
@ -35,19 +35,15 @@ describe('SetStatusModalWrapper', () => {
mocks: {
$toast,
},
provide: {
glFeatures: { improvedEmojiPicker },
},
});
};
const findModal = () => wrapper.find(GlModal);
const findFormField = (field) => wrapper.find(`[name="user[status][${field}]"]`);
const findClearStatusButton = () => wrapper.find('.js-clear-user-status-button');
const findNoEmojiPlaceholder = () => wrapper.find('.js-no-emoji-placeholder');
const findToggleEmojiButton = () => wrapper.find('.js-toggle-emoji-menu');
const findAvailabilityCheckbox = () => wrapper.find(GlFormCheckbox);
const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]');
const getEmojiPicker = () => wrapper.findComponent(EmojiPicker);
const initModal = async ({ mockOnUpdateSuccess = true, mockOnUpdateFailure = true } = {}) => {
const modal = findModal();
@ -95,12 +91,6 @@ describe('SetStatusModalWrapper', () => {
expect(findClearStatusButton().isVisible()).toBe(true);
});
it('clicking the toggle emoji button displays the emoji list', () => {
expect(wrapper.vm.showEmojiMenu).not.toHaveBeenCalled();
findToggleEmojiButton().trigger('click');
expect(wrapper.vm.showEmojiMenu).toHaveBeenCalled();
});
it('displays the clear status at dropdown', () => {
expect(wrapper.find('[data-testid="clear-status-at-dropdown"]').exists()).toBe(true);
});
@ -108,16 +98,6 @@ describe('SetStatusModalWrapper', () => {
it('does not display the clear status at message', () => {
expect(findClearStatusAtMessage().exists()).toBe(false);
});
});
describe('improvedEmojiPicker is true', () => {
const getEmojiPicker = () => wrapper.findComponent(EmojiPicker);
beforeEach(async () => {
await initEmojiMock();
wrapper = createComponent({}, true);
return initModal();
});
it('renders emoji picker dropdown with custom positioning', () => {
expect(getEmojiPicker().props()).toMatchObject({
@ -147,10 +127,6 @@ describe('SetStatusModalWrapper', () => {
it('hides the clear status button', () => {
expect(findClearStatusButton().isVisible()).toBe(false);
});
it('shows the placeholder emoji', () => {
expect(findNoEmojiPlaceholder().isVisible()).toBe(true);
});
});
describe('with no currentEmoji set', () => {
@ -163,22 +139,6 @@ describe('SetStatusModalWrapper', () => {
it('does not set the hidden status emoji field', () => {
expect(findFormField('emoji').element.value).toBe('');
});
it('hides the placeholder emoji', () => {
expect(findNoEmojiPlaceholder().isVisible()).toBe(false);
});
describe('with no currentMessage set', () => {
beforeEach(async () => {
await initEmojiMock();
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
return initModal();
});
it('shows the placeholder emoji', () => {
expect(findNoEmojiPlaceholder().isVisible()).toBe(true);
});
});
});
describe('with currentClearStatusAfter set', () => {

View file

@ -218,65 +218,88 @@ exports[`vue_shared/components/awards_list default matches snapshot 1`] = `
<div
class="award-menu-holder gl-my-2"
>
<button
aria-label="Add reaction"
class="btn add-reaction-button js-add-award btn-default btn-md gl-button js-test-add-button-class"
<div
class="emoji-picker"
data-testid="emoji-picker"
title="Add reaction"
type="button"
>
<!---->
<!---->
<span
class="gl-button-text"
<div
boundary="scrollParent"
class="dropdown b-dropdown gl-new-dropdown btn-group"
id="__BVID__13"
lazy=""
menu-class="dropdown-extended-height"
no-flip=""
>
<span
class="reaction-control-icon reaction-control-icon-neutral"
<!---->
<button
aria-expanded="false"
aria-haspopup="true"
class="btn dropdown-toggle btn-default btn-md add-reaction-button btn-icon gl-relative! gl-button gl-dropdown-toggle btn-default-secondary"
id="__BVID__13__BV_toggle_"
type="button"
>
<svg
aria-hidden="true"
class="gl-icon s16"
data-testid="slight-smile-icon"
role="img"
<span
class="gl-sr-only"
>
<use
href="#slight-smile"
/>
</svg>
</span>
<span
class="reaction-control-icon reaction-control-icon-positive"
Add reaction
</span>
<span
class="reaction-control-icon reaction-control-icon-neutral"
>
<svg
aria-hidden="true"
class="gl-icon s16"
data-testid="slight-smile-icon"
role="img"
>
<use
href="#slight-smile"
/>
</svg>
</span>
<span
class="reaction-control-icon reaction-control-icon-positive"
>
<svg
aria-hidden="true"
class="gl-icon s16"
data-testid="smiley-icon"
role="img"
>
<use
href="#smiley"
/>
</svg>
</span>
<span
class="reaction-control-icon reaction-control-icon-super-positive"
>
<svg
aria-hidden="true"
class="gl-icon s16"
data-testid="smile-icon"
role="img"
>
<use
href="#smile"
/>
</svg>
</span>
</button>
<ul
aria-labelledby="__BVID__13__BV_toggle_"
class="dropdown-menu dropdown-extended-height dropdown-menu-right"
role="menu"
tabindex="-1"
>
<svg
aria-hidden="true"
class="gl-icon s16"
data-testid="smiley-icon"
role="img"
>
<use
href="#smiley"
/>
</svg>
</span>
<span
class="reaction-control-icon reaction-control-icon-super-positive"
>
<svg
aria-hidden="true"
class="gl-icon s16"
data-testid="smile-icon"
role="img"
>
<use
href="#smile"
/>
</svg>
</span>
</span>
</button>
<!---->
</ul>
</div>
</div>
</div>
</div>
`;

View file

@ -76,7 +76,7 @@ describe('vue_shared/components/awards_list', () => {
count: Number(x.find('.js-counter').text()),
};
});
const findAddAwardButton = () => wrapper.find('.js-add-award');
const findAddAwardButton = () => wrapper.find('[data-testid="emoji-picker"]');
describe('default', () => {
beforeEach(() => {
@ -151,7 +151,6 @@ describe('vue_shared/components/awards_list', () => {
const btn = findAddAwardButton();
expect(btn.exists()).toBe(true);
expect(btn.classes(TEST_ADD_BUTTON_CLASS)).toBe(true);
});
});

View file

@ -53,5 +53,15 @@ RSpec.describe Resolvers::WorkItems::TypesResolver do
it_behaves_like 'a work item type resolver'
end
context 'when parent is not a group or project' do
let(:object) { 'not a project/group' }
it 'returns nil because of feature flag check' do
result = resolve(described_class, obj: object, args: {})
expect(result).to be_nil
end
end
end
end

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::PipelineHelper do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:raw_pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
let_it_be(:pipeline) { Ci::PipelinePresenter.new(raw_pipeline, current_user: user)}
describe '#js_pipeline_tabs_data' do
subject(:pipeline_tabs_data) { helper.js_pipeline_tabs_data(project, pipeline) }
it 'returns pipeline tabs data' do
expect(pipeline_tabs_data).to include({
can_generate_codequality_reports: pipeline.can_generate_codequality_reports?.to_json,
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
pipeline_project_path: project.full_path
})
end
end
end

View file

@ -64,6 +64,34 @@ RSpec.describe Gitlab::GonHelper do
end
end
describe '#push_force_frontend_feature_flag' do
let(:gon) { class_double('Gon') }
before do
skip_feature_flags_yaml_validation
allow(helper)
.to receive(:gon)
.and_return(gon)
end
it 'pushes a feature flag to the frontend with the provided value' do
expect(gon)
.to receive(:push)
.with({ features: { 'myFeatureFlag' => true } }, true)
helper.push_force_frontend_feature_flag(:my_feature_flag, true)
end
it 'pushes a disabled feature flag if provided value is nil' do
expect(gon)
.to receive(:push)
.with({ features: { 'myFeatureFlag' => false } }, true)
helper.push_force_frontend_feature_flag(:my_feature_flag, nil)
end
end
describe '#default_avatar_url' do
it 'returns an absolute URL' do
url = helper.default_avatar_url

View file

@ -3,6 +3,13 @@
require 'spec_helper'
RSpec.describe BulkImport, type: :model do
let_it_be(:created_bulk_import) { create(:bulk_import, :created) }
let_it_be(:started_bulk_import) { create(:bulk_import, :started) }
let_it_be(:finished_bulk_import) { create(:bulk_import, :finished) }
let_it_be(:failed_bulk_import) { create(:bulk_import, :failed) }
let_it_be(:stale_created_bulk_import) { create(:bulk_import, :created, created_at: 3.days.ago) }
let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, created_at: 3.days.ago) }
describe 'associations' do
it { is_expected.to belong_to(:user).required }
it { is_expected.to have_one(:configuration) }
@ -16,9 +23,15 @@ RSpec.describe BulkImport, type: :model do
it { is_expected.to define_enum_for(:source_type).with_values(%i[gitlab]) }
end
describe '.stale scope' do
subject { described_class.stale }
it { is_expected.to contain_exactly(stale_created_bulk_import, stale_started_bulk_import) }
end
describe '.all_human_statuses' do
it 'returns all human readable entity statuses' do
expect(described_class.all_human_statuses).to contain_exactly('created', 'started', 'finished', 'failed')
expect(described_class.all_human_statuses).to contain_exactly('created', 'started', 'finished', 'failed', 'timeout')
end
end

View file

@ -151,7 +151,7 @@ RSpec.describe BulkImports::Entity, type: :model do
describe '.all_human_statuses' do
it 'returns all human readable entity statuses' do
expect(described_class.all_human_statuses).to contain_exactly('created', 'started', 'finished', 'failed')
expect(described_class.all_human_statuses).to contain_exactly('created', 'started', 'finished', 'failed', 'timeout')
end
end

View file

@ -302,6 +302,19 @@ RSpec.describe ContainerRepository, :aggregate_failures do
expect(repository.migration_aborted_in_state).to eq('importing')
expect(repository).to be_import_aborted
end
context 'above the max retry limit' do
before do
stub_application_setting(container_registry_import_max_retries: 1)
end
it 'skips the migration' do
expect { subject }.to change { repository.migration_skipped_at }
expect(repository.reload).to be_import_skipped
expect(repository.migration_skipped_reason).to eq('too_many_retries')
end
end
end
describe '#skip_import' do
@ -309,7 +322,7 @@ RSpec.describe ContainerRepository, :aggregate_failures do
subject { repository.skip_import(reason: :too_many_retries) }
it_behaves_like 'transitioning from allowed states', ContainerRepository::ABORTABLE_MIGRATION_STATES
it_behaves_like 'transitioning from allowed states', ContainerRepository::SKIPPABLE_MIGRATION_STATES
it 'sets migration_skipped_at and migration_skipped_reason' do
expect { subject }.to change { repository.reload.migration_skipped_at }
@ -1119,6 +1132,17 @@ RSpec.describe ContainerRepository, :aggregate_failures do
end
end
context 'not found response' do
let(:response) { :not_found }
it 'aborts the migration' do
expect(subject).to eq(false)
expect(container_repository).to be_import_skipped
expect(container_repository.reload.migration_skipped_reason).to eq('not_found')
end
end
context 'other response' do
let(:response) { :error }
@ -1136,6 +1160,30 @@ RSpec.describe ContainerRepository, :aggregate_failures do
end
end
describe '#retried_too_many_times?' do
subject { repository.retried_too_many_times? }
before do
stub_application_setting(container_registry_import_max_retries: 3)
end
context 'migration_retries_count is equal or greater than max_retries' do
before do
repository.update_column(:migration_retries_count, 3)
end
it { is_expected.to eq(true) }
end
context 'migration_retries_count is lower than max_retries' do
before do
repository.update_column(:migration_retries_count, 2)
end
it { is_expected.to eq(false) }
end
end
context 'with repositories' do
let_it_be_with_reload(:repository) { create(:container_repository, :cleanup_unscheduled) }
let_it_be(:other_repository) { create(:container_repository, :cleanup_unscheduled) }

View file

@ -3248,4 +3248,46 @@ RSpec.describe Group do
it_behaves_like 'no effective expiration interval'
end
end
describe '#work_items_feature_flag_enabled?' do
let_it_be(:root_group) { create(:group) }
let_it_be(:group) { create(:group, parent: root_group) }
let_it_be(:project) { create(:project, group: group) }
subject { group.work_items_feature_flag_enabled? }
context 'when work_items FF is enabled for the root group' do
before do
stub_feature_flags(work_items: root_group)
end
it { is_expected.to be_truthy }
end
context 'when work_items FF is enabled for the group' do
before do
stub_feature_flags(work_items: group)
end
it { is_expected.to be_truthy }
context 'when root_group is the actor' do
it 'is not enabled if the FF is enabled for a child' do
expect(root_group).not_to be_work_items_feature_flag_enabled
end
end
end
context 'when work_items FF is disabled globally' do
before do
stub_feature_flags(work_items: false)
end
it { is_expected.to be_falsey }
end
context 'when work_items FF is enabled globally' do
it { is_expected.to be_truthy }
end
end
end

View file

@ -8011,6 +8011,62 @@ RSpec.describe Project, factory_default: :keep do
end
end
describe '#work_items_feature_flag_enabled?' do
shared_examples 'project checking work_items feature flag' do
context 'when work_items FF is disabled globally' do
before do
stub_feature_flags(work_items: false)
end
it { is_expected.to be_falsey }
end
context 'when work_items FF is enabled for the project' do
before do
stub_feature_flags(work_items: project)
end
it { is_expected.to be_truthy }
end
context 'when work_items FF is enabled globally' do
it { is_expected.to be_truthy }
end
end
subject { project.work_items_feature_flag_enabled? }
context 'when a project does not belong to a group' do
let_it_be(:project) { create(:project, namespace: namespace) }
it_behaves_like 'project checking work_items feature flag'
end
context 'when project belongs to a group' do
let_it_be(:root_group) { create(:group) }
let_it_be(:group) { create(:group, parent: root_group) }
let_it_be(:project) { create(:project, group: group) }
it_behaves_like 'project checking work_items feature flag'
context 'when work_items FF is enabled for the root group' do
before do
stub_feature_flags(work_items: root_group)
end
it { is_expected.to be_truthy }
end
context 'when work_items FF is enabled for the group' do
before do
stub_feature_flags(work_items: group)
end
it { is_expected.to be_truthy }
end
end
end
describe 'serialization' do
let(:object) { build(:project) }

View file

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Work Items' do
let_it_be(:work_item) { create(:work_item) }
let_it_be(:developer) { create(:user) }
before_all do
work_item.project.add_developer(developer)
end
describe 'GET /:namespace/:project/work_items/:id' do
before do
sign_in(developer)
end
context 'when the work_items feature flag is enabled' do
it 'renders index' do
get project_work_items_url(work_item.project, work_items_path: work_item.id)
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when the work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
end
it 'returns 404' do
get project_work_items_url(work_item.project, work_items_path: work_item.id)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end

View file

@ -13,6 +13,7 @@ RSpec.describe 'projects/pipelines/show' do
before do
assign(:project, project)
assign(:pipeline, presented_pipeline)
stub_feature_flags(pipeline_tabs_vue: false)
end
context 'when pipeline has errors' do

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::StuckImportWorker do
let_it_be(:created_bulk_import) { create(:bulk_import, :created) }
let_it_be(:started_bulk_import) { create(:bulk_import, :started) }
let_it_be(:stale_created_bulk_import) { create(:bulk_import, :created, created_at: 3.days.ago) }
let_it_be(:stale_started_bulk_import) { create(:bulk_import, :started, created_at: 3.days.ago) }
let_it_be(:stale_created_bulk_import_entity) { create(:bulk_import_entity, :created, created_at: 3.days.ago) }
let_it_be(:stale_started_bulk_import_entity) { create(:bulk_import_entity, :started, created_at: 3.days.ago) }
subject { described_class.new.perform }
describe 'perform' do
it 'updates the status of bulk imports to timeout' do
expect { subject }.to change { stale_created_bulk_import.reload.status }.from(0).to(3)
.and change { stale_started_bulk_import.reload.status }.from(1).to(3)
end
it 'updates the status of bulk import entities to timeout' do
expect { subject }.to change { stale_created_bulk_import_entity.reload.status }.from(0).to(3)
.and change { stale_started_bulk_import_entity.reload.status }.from(1).to(3)
end
it 'does not update the status of non-stale records' do
expect { subject }.to not_change { created_bulk_import.reload.status }
.and not_change { started_bulk_import.reload.status }
end
end
end