Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-27 12:10:16 +00:00
parent 1b9f574b89
commit af3904f9d0
77 changed files with 860 additions and 218 deletions

View File

@ -63,7 +63,7 @@ workflow:
variables:
PG_VERSION: "12"
DEFAULT_CI_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}.patched-golang-1.17-node-16.14-postgresql-${PG_VERSION}:rubygems-3.2-git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-yarn-1.22-graphicsmagick-1.3.36"
DEFAULT_CI_IMAGE: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}.patched-golang-${GO_VERSION}-node-16.14-postgresql-${PG_VERSION}:rubygems-3.2-git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-yarn-1.22-graphicsmagick-1.3.36"
RAILS_ENV: "test"
NODE_ENV: "test"
BUNDLE_WITHOUT: "production:development"
@ -80,6 +80,7 @@ variables:
CHROME_VERSION: "103"
DOCKER_VERSION: "20.10.14"
RUBY_VERSION: "2.7"
GO_VERSION: "1.18"
TMP_TEST_FOLDER: "${CI_PROJECT_DIR}/tmp/tests"
GITLAB_WORKHORSE_FOLDER: "gitlab-workhorse"

View File

@ -1,6 +1,6 @@
workhorse:verify:
extends: .workhorse:rules:workhorse
image: ${GITLAB_DEPENDENCY_PROXY}golang:1.17
image: ${GITLAB_DEPENDENCY_PROXY}golang:1.18
stage: test
needs: []
script:
@ -12,7 +12,7 @@ workhorse:verify:
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}-golang-${GO_VERSION}:git-2.36
variables:
GITALY_ADDRESS: "tcp://127.0.0.1:8075"
GO_VERSION: "1.17"
GO_VERSION: "1.18"
stage: test
needs:
- setup-test-env

View File

@ -0,0 +1,21 @@
<script>
import MessagesTable from './messages_table.vue';
export default {
name: 'BroadcastMessagesBase',
components: {
MessagesTable,
},
props: {
messages: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div>
<messages-table v-if="messages.length > 0" :messages="messages" />
</div>
</template>

View File

@ -0,0 +1,21 @@
<script>
import MessagesTableRow from './messages_table_row.vue';
export default {
name: 'MessagesTable',
components: {
MessagesTableRow,
},
props: {
messages: {
type: Array,
required: true,
},
},
};
</script>
<template>
<div>
<messages-table-row v-for="message in messages" :key="message.id" :message="message" />
</div>
</template>

View File

@ -0,0 +1,16 @@
<script>
export default {
name: 'MessagesTableRow',
props: {
message: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div>
{{ message.id }}
</div>
</template>

View File

@ -0,0 +1,19 @@
import Vue from 'vue';
import BroadcastMessagesBase from './components/base.vue';
export default () => {
const el = document.querySelector('#js-broadcast-messages');
const { messages } = el.dataset;
return new Vue({
el,
name: 'BroadcastMessagesBase',
render(createElement) {
return createElement(BroadcastMessagesBase, {
props: {
messages: JSON.parse(messages),
},
});
},
});
};

View File

@ -1,5 +1,10 @@
import initBroadcastMessages from '~/admin/broadcast_messages';
import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
import initBroadcastMessagesForm from './broadcast_message';
initBroadcastMessagesForm();
initDeprecatedRemoveRowBehavior();
if (gon.features.vueBroadcastMessages) {
initBroadcastMessages();
} else {
initBroadcastMessagesForm();
initDeprecatedRemoveRowBehavior();
}

View File

@ -9,6 +9,7 @@ import {
GlFormInput,
GlFormSelect,
} from '@gitlab/ui';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import csrf from '~/lib/utils/csrf';
import { setUrlFragment } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
@ -33,6 +34,29 @@ const MARKDOWN_LINK_TEXT = {
org: '[[page-slug]]',
};
function getPagePath(pageInfo) {
return pageInfo.persisted ? pageInfo.path : pageInfo.createPath;
}
const autosaveKey = (pageInfo, field) => {
const path = pageInfo.persisted ? pageInfo.path : pageInfo.createPath;
return `${path}/${field}`;
};
const titleAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'title');
const formatAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'format');
const contentAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'content');
const commitAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'commit');
const getTitle = (pageInfo) => getDraft(titleAutosaveKey(pageInfo)) || pageInfo.title?.trim() || '';
const getFormat = (pageInfo) =>
getDraft(formatAutosaveKey(pageInfo)) || pageInfo.format || 'markdown';
const getContent = (pageInfo) => getDraft(contentAutosaveKey(pageInfo)) || pageInfo.content || '';
const getCommitMessage = (pageInfo) =>
getDraft(commitAutosaveKey(pageInfo)) || pageInfo.commitMessage || '';
const getIsFormDirty = (pageInfo) => Boolean(getDraft(titleAutosaveKey(pageInfo)));
export default {
i18n: {
title: {
@ -87,13 +111,14 @@ export default {
data() {
return {
editingMode: 'source',
title: this.pageInfo.title?.trim() || '',
format: this.pageInfo.format || 'markdown',
content: this.pageInfo.content || '',
commitMessage: '',
isDirty: false,
title: getTitle(this.pageInfo),
format: getFormat(this.pageInfo),
content: getContent(this.pageInfo),
commitMessage: getCommitMessage(this.pageInfo),
contentEditorEmpty: false,
isContentEditorActive: false,
switchEditingControlDisabled: false,
isFormDirty: getIsFormDirty(this.pageInfo),
};
},
computed: {
@ -104,7 +129,7 @@ export default {
return csrf.token;
},
formAction() {
return this.pageInfo.persisted ? this.pageInfo.path : this.pageInfo.createPath;
return getPagePath(this.pageInfo);
},
helpPath() {
return setUrlFragment(
@ -151,7 +176,7 @@ export default {
},
},
mounted() {
this.updateCommitMessage();
if (!this.commitMessage) this.updateCommitMessage();
window.addEventListener('beforeunload', this.onPageUnload);
},
@ -160,6 +185,8 @@ export default {
},
methods: {
async handleFormSubmit(e) {
this.isFormDirty = false;
e.preventDefault();
this.trackFormSubmit();
@ -169,18 +196,33 @@ export default {
await this.$nextTick();
e.target.submit();
this.isDirty = false;
},
onPageUnload(event) {
if (!this.isDirty) return undefined;
updateDrafts() {
updateDraft(titleAutosaveKey(this.pageInfo), this.title);
updateDraft(formatAutosaveKey(this.pageInfo), this.format);
updateDraft(contentAutosaveKey(this.pageInfo), this.content);
updateDraft(commitAutosaveKey(this.pageInfo), this.commitMessage);
},
event.preventDefault();
clearDrafts() {
clearDraft(titleAutosaveKey(this.pageInfo));
clearDraft(formatAutosaveKey(this.pageInfo));
clearDraft(contentAutosaveKey(this.pageInfo));
clearDraft(commitAutosaveKey(this.pageInfo));
},
// eslint-disable-next-line no-param-reassign
event.returnValue = '';
return '';
handleContentEditorChange({ empty, markdown }) {
this.contentEditorEmpty = empty;
this.content = markdown;
},
onPageUnload() {
if (this.isFormDirty) {
this.updateDrafts();
} else {
this.clearDrafts();
}
},
updateCommitMessage() {
@ -222,10 +264,6 @@ export default {
trackContentEditorLoaded() {
this.track(CONTENT_EDITOR_LOADED_ACTION);
},
checkDirty(markdown) {
this.isDirty = this.pageInfo.content !== markdown;
},
},
};
</script>
@ -236,6 +274,7 @@ export default {
method="post"
class="wiki-form common-note-form gl-mt-3 js-quick-submit"
@submit="handleFormSubmit"
@input="isFormDirty = true"
>
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<input v-if="pageInfo.persisted" type="hidden" name="_method" value="put" />
@ -306,7 +345,6 @@ export default {
form-field-name="wiki[content]"
@contentEditor="notifyContentEditorActive"
@markdownField="notifyContentEditorInactive"
@input="checkDirty"
/>
<div class="form-text gl-text-gray-600">
<gl-sprintf
@ -358,9 +396,14 @@ export default {
:disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
>
<gl-button data-testid="wiki-cancel-button" :href="cancelFormPath" class="float-right">{{
$options.i18n.cancel
}}</gl-button>
<gl-button
data-testid="wiki-cancel-button"
:href="cancelFormPath"
class="float-right"
@click="isFormDirty = false"
>
{{ $options.i18n.cancel }}</gl-button
>
</div>
</gl-form>
</template>

View File

@ -1,8 +1,15 @@
<script>
import { GlFormGroup, GlDropdownItem, GlSprintf } from '@gitlab/ui';
import {
GlCollapse,
GlLink,
GlFormGroup,
GlFormTextarea,
GlDropdownItem,
GlSprintf,
} from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapState, mapActions, mapGetters } from 'vuex';
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_TAGS } from '~/ref/constants';
import FormFieldContainer from './form_field_container.vue';
@ -10,7 +17,10 @@ import FormFieldContainer from './form_field_container.vue';
export default {
name: 'TagFieldNew',
components: {
GlCollapse,
GlFormGroup,
GlFormTextarea,
GlLink,
RefSelector,
FormFieldContainer,
GlDropdownItem,
@ -41,6 +51,14 @@ export default {
this.updateShowCreateFrom(false);
},
},
tagMessage: {
get() {
return this.release.tagMessage;
},
set(tagMessage) {
this.updateReleaseTagMessage(tagMessage);
},
},
createFromModel: {
get() {
return this.createFrom;
@ -70,6 +88,7 @@ export default {
methods: {
...mapActions('editNew', [
'updateReleaseTagName',
'updateReleaseTagMessage',
'updateCreateFrom',
'fetchTagNotes',
'updateShowCreateFrom',
@ -113,9 +132,20 @@ export default {
noRefSelected: __('No source selected'),
searchPlaceholder: __('Search branches, tags, and commits'),
dropdownHeader: __('Select source'),
label: __('Create from'),
description: __('Existing branch name, tag, or commit SHA'),
},
annotatedTag: {
label: s__('CreateGitTag|Set tag message'),
description: s__(
'CreateGitTag|Add a message to the tag. Leaving this blank creates a %{linkStart}lightweight tag%{linkEnd}.',
),
},
},
tagMessageId: uniqueId('tag-message-'),
tagNameEnabledRefTypes: [REF_TYPE_TAGS],
gitTagDocsLink: 'https://git-scm.com/book/en/v2/Git-Basics-Tagging/',
};
</script>
<template>
@ -156,23 +186,45 @@ export default {
</ref-selector>
</form-field-container>
</gl-form-group>
<gl-form-group
v-if="showCreateFrom"
:label="__('Create from')"
:label-for="createFromSelectorId"
data-testid="create-from-field"
>
<form-field-container>
<ref-selector
:id="createFromSelectorId"
v-model="createFromModel"
:project-id="projectId"
:translations="$options.translations.createFrom"
/>
</form-field-container>
<template #description>
{{ __('Existing branch name, tag, or commit SHA') }}
</template>
</gl-form-group>
<gl-collapse :visible="showCreateFrom">
<div class="gl-pl-6 gl-border-l-1 gl-border-l-solid gl-border-gray-300">
<gl-form-group
v-if="showCreateFrom"
:label="$options.translations.createFrom.label"
:label-for="createFromSelectorId"
data-testid="create-from-field"
>
<form-field-container>
<ref-selector
:id="createFromSelectorId"
v-model="createFromModel"
:project-id="projectId"
:translations="$options.translations.createFrom"
/>
</form-field-container>
<template #description>{{ $options.translations.createFrom.description }}</template>
</gl-form-group>
<gl-form-group
v-if="showCreateFrom"
:label="$options.translations.annotatedTag.label"
:label-for="$options.tagMessageId"
data-testid="annotated-tag-message-field"
>
<gl-form-textarea :id="$options.tagMessageId" v-model="tagMessage" />
<template #description>
<gl-sprintf :message="$options.translations.annotatedTag.description">
<template #link="{ content }">
<gl-link
:href="$options.gitTagDocsLink"
rel="noopener noreferrer"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</template>
</gl-form-group>
</div>
</gl-collapse>
</div>
</template>

View File

@ -57,6 +57,9 @@ export const fetchRelease = async ({ commit, state }) => {
export const updateReleaseTagName = ({ commit }, tagName) =>
commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
export const updateReleaseTagMessage = ({ commit }, tagMessage) =>
commit(types.UPDATE_RELEASE_TAG_MESSAGE, tagMessage);
export const updateCreateFrom = ({ commit }, createFrom) =>
commit(types.UPDATE_CREATE_FROM, createFrom);

View File

@ -145,6 +145,7 @@ export const releaseCreateMutatationVariables = (state, getters) => {
input: {
...getters.releaseUpdateMutatationVariables.input,
ref: state.createFrom,
tagMessage: state.release.tagMessage,
assets: {
links: getters.releaseLinksToCreate.map(({ name, url, linkType }) => ({
name: name.trim(),

View File

@ -5,6 +5,7 @@ export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
export const UPDATE_RELEASE_TAG_NAME = 'UPDATE_RELEASE_TAG_NAME';
export const UPDATE_RELEASE_TAG_MESSAGE = 'UPDATE_RELEASE_TAG_MESSAGE';
export const UPDATE_CREATE_FROM = 'UPDATE_CREATE_FROM';
export const UPDATE_SHOW_CREATE_FROM = 'UPDATE_SHOW_CREATE_FROM';
export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';

View File

@ -10,6 +10,7 @@ export default {
[types.INITIALIZE_EMPTY_RELEASE](state) {
state.release = {
tagName: state.tagName,
tagMessage: '',
name: '',
description: '',
milestones: [],
@ -40,6 +41,9 @@ export default {
[types.UPDATE_RELEASE_TAG_NAME](state, tagName) {
state.release.tagName = tagName;
},
[types.UPDATE_RELEASE_TAG_MESSAGE](state, tagMessage) {
state.release.tagMessage = tagMessage;
},
[types.UPDATE_CREATE_FROM](state, createFrom) {
state.createFrom = createFrom;
},

View File

@ -37,7 +37,7 @@ export default ({
* When creating a new release, this is the default from the URL
*/
tagName,
showCreateFrom: !tagName,
showCreateFrom: false,
defaultBranch,
createFrom: defaultBranch,

View File

@ -10,6 +10,8 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def index
push_frontend_feature_flag(:vue_broadcast_messages, current_user)
@broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page])
@broadcast_message = BroadcastMessage.new
end

View File

@ -50,16 +50,15 @@ module Projects
track_event(:error_enable_cloudsql_services)
flash[:error] = error_message(enable_response[:message])
else
permitted_params = params.permit(:gcp_project, :ref, :database_version, :tier)
create_response = ::GoogleCloud::CreateCloudsqlInstanceService
.new(project, current_user, create_service_params(permitted_params))
.new(project, current_user, create_service_params)
.execute
if create_response[:status] == :error
track_event(:error_create_cloudsql_instance)
flash[:warning] = error_message(create_response[:message])
else
track_event(:create_cloudsql_instance, permitted_params.to_s)
track_event(:create_cloudsql_instance, permitted_params_create.to_s)
flash[:notice] = success_message
end
end
@ -69,17 +68,25 @@ module Projects
private
def enable_service_params
{ google_oauth2_token: token_in_session }
def permitted_params_create
params.permit(:gcp_project, :ref, :database_version, :tier)
end
def create_service_params(permitted_params)
def enable_service_params
{
google_oauth2_token: token_in_session,
gcp_project_id: permitted_params[:gcp_project],
environment_name: permitted_params[:ref],
database_version: permitted_params[:database_version],
tier: permitted_params[:tier]
gcp_project_id: permitted_params_create[:gcp_project],
environment_name: permitted_params_create[:ref]
}
end
def create_service_params
{
google_oauth2_token: token_in_session,
gcp_project_id: permitted_params_create[:gcp_project],
environment_name: permitted_params_create[:ref],
database_version: permitted_params_create[:database_version],
tier: permitted_params_create[:tier]
}
end

View File

@ -667,6 +667,7 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :arkose_labs_public_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :arkose_labs_private_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :cube_api_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :jitsu_administrator_password, encryption_options_base_32_aes_256_gcm
validates :disable_feed_token,
inclusion: { in: [true, false], message: _('must be a boolean value') }

View File

@ -16,6 +16,7 @@ module Ci
validates :file, presence: true, file_size: { maximum: FILE_SIZE_LIMIT }
validates :checksum, :file_store, :name, :project_id, presence: true
validates :name, uniqueness: { scope: :project }
validates :metadata, json_schema: { filename: "ci_secure_file_metadata" }, allow_nil: true
after_initialize :generate_key_data
before_validation :assign_checksum
@ -23,6 +24,8 @@ module Ci
scope :order_by_created_at, -> { order(created_at: :desc) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
serialize :metadata, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
default_value_for(:file_store) { Ci::SecureFileUploader.default_store }
mount_file_store_uploader Ci::SecureFileUploader

View File

@ -2,6 +2,8 @@
class ProjectAuthorization < ApplicationRecord
BATCH_SIZE = 1000
SLEEP_DELAY = 0.1
extend SuppressCompositePrimaryKeyWarning
include FromUnion
@ -55,12 +57,16 @@ class ProjectAuthorization < ApplicationRecord
end
private_class_method def self.add_delay_between_batches?(entire_size:, batch_size:)
# The reason for adding a delay is to give the replica database enough time to
# catch up with the primary when large batches of records are being added/removed.
# Hance, we add a delay only if the GitLab installation has a replica database configured.
entire_size > batch_size &&
!::Gitlab::Database::LoadBalancing.primary_only? &&
Feature.enabled?(:enable_minor_delay_during_project_authorizations_refresh)
end
private_class_method def self.perform_delay
sleep(0.1)
sleep(SLEEP_DELAY)
end
end

View File

@ -21,6 +21,7 @@ class ProjectSetting < ApplicationRecord
validates :merge_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH }
validates :squash_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH }
validates :target_platforms, inclusion: { in: ALLOWED_TARGET_PLATFORMS }
validates :suggested_reviewers_enabled, inclusion: { in: [true, false] }
validate :validates_mr_default_target_self

View File

@ -3,7 +3,7 @@
module GoogleCloud
class EnableCloudsqlService < ::GoogleCloud::BaseService
def execute
return no_projects_error if unique_gcp_project_ids.empty?
create_or_replace_project_vars(environment_name, 'GCP_PROJECT_ID', gcp_project_id, ci_var_protected?)
unique_gcp_project_ids.each do |gcp_project_id|
google_api_client.enable_cloud_sql_admin(gcp_project_id)
@ -18,8 +18,8 @@ module GoogleCloud
private
def no_projects_error
error("No GCP projects found. Configure a service account or GCP_PROJECT_ID CI variable.")
def ci_var_protected?
ProtectedBranch.protected?(project, environment_name) || ProtectedTag.protected?(project, environment_name)
end
end
end

View File

@ -0,0 +1,22 @@
{
"description": "CI Secure File Metadata",
"type": "object",
"properties": {
"id": { "type": "string" },
"team_name": { "type": "string" },
"team_id": { "type": "string" },
"app_name": { "type": "string" },
"app_id": { "type": "string" },
"app_id_prefix": { "type": "string" },
"xcode_managed": { "type": "boolean" },
"entitlements": { "type": "object" },
"devices": { "type": "array" },
"certificate_ids": { "type": "array" },
"issuer": { "type": "object" },
"subject": { "type": "object" }
},
"additionalProperties": true,
"required": [
"id"
]
}

View File

@ -31,4 +31,4 @@
.form-text.text-muted
= _('Only required if not using role instance credentials.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
= f.submit _('Save changes'), pajamas_button: true

View File

@ -21,4 +21,4 @@
.form-group
= f.gitlab_ui_checkbox_component :user_deactivation_emails_enabled, _('Enable user deactivation emails'), help_text: _('Send emails to users upon account deactivation.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
= f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }

View File

@ -26,4 +26,4 @@
= s_('Gitpod|The URL to your Gitpod instance configured to read your GitLab projects, such as https://gitpod.example.com.')
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('integration/gitpod', anchor: 'enable-gitpod-in-your-user-settings') }
= s_('Gitpod|To use the integration, each user must also enable Gitpod on their GitLab account. %{link_start}How do I enable it?%{link_end} ').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
= f.submit _('Save changes'), pajamas_button: true

View File

@ -15,5 +15,5 @@
- time_tracking_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: time_tracking_help_link }
= f.gitlab_ui_checkbox_component :time_tracking_limit_to_hours, _('Limit display of time tracking units to hours.'), help_text: _('Display time tracking in issues in total hours only. %{link_start}What is time tracking?%{link_end}').html_safe % { link_start: time_tracking_help_link_start, link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
= f.submit _('Save changes'), pajamas_button: true

View File

@ -0,0 +1,38 @@
- targeted_broadcast_messages_enabled = Feature.enabled?(:role_targeted_broadcast_messages)
- if @broadcast_messages.any?
.table-responsive
%table.table.b-table.gl-table
%thead
%tr
%th= _('Status')
%th= _('Preview')
%th= _('Starts')
%th= _('Ends')
- if targeted_broadcast_messages_enabled
%th= _('Target roles')
%th= _('Target Path')
%th= _('Type')
%th &nbsp;
%tbody
- @broadcast_messages.each do |message|
%tr
%td
= broadcast_message_status(message)
%td
= broadcast_message(message, preview: true)
%td
= message.starts_at
%td
= message.ends_at
- if targeted_broadcast_messages_enabled
%td
= target_access_levels_display(message.target_access_levels)
%td
= message.target_path
%td
= message.broadcast_type.capitalize
%td.gl-white-space-nowrap<
= link_to sprite_icon('pencil', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: _('Edit'), class: 'btn btn-icon gl-button'
= link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: _('Remove'), class: 'js-remove-tr btn btn-icon gl-button btn-danger gl-ml-3'
= paginate @broadcast_messages, theme: 'gitlab'

View File

@ -1,49 +1,15 @@
- breadcrumb_title _("Messages")
- page_title _("Broadcast Messages")
- targeted_broadcast_messages_enabled = Feature.enabled?(:role_targeted_broadcast_messages)
- vue_app_enabled = Feature.enabled?(:vue_broadcast_messages, current_user)
%h1.page-title.gl-font-size-h-display
= _('Broadcast Messages')
%p.light
= _('Use banners and notifications to notify your users about scheduled maintenance, recent upgrades, and more.')
= render 'form'
%br.clearfix
- if @broadcast_messages.any?
.table-responsive
%table.table.b-table.gl-table
%thead
%tr
%th= _('Status')
%th= _('Preview')
%th= _('Starts')
%th= _('Ends')
- if targeted_broadcast_messages_enabled
%th= _('Target roles')
%th= _('Target Path')
%th= _('Type')
%th &nbsp;
%tbody
- @broadcast_messages.each do |message|
%tr
%td
= broadcast_message_status(message)
%td
= broadcast_message(message, preview: true)
%td
= message.starts_at
%td
= message.ends_at
- if targeted_broadcast_messages_enabled
%td
= target_access_levels_display(message.target_access_levels)
%td
= message.target_path
%td
= message.broadcast_type.capitalize
%td.gl-white-space-nowrap<
= link_to sprite_icon('pencil', css_class: 'gl-icon'), edit_admin_broadcast_message_path(message), title: _('Edit'), class: 'btn btn-icon gl-button'
= link_to sprite_icon('remove', css_class: 'gl-icon'), admin_broadcast_message_path(message), method: :delete, remote: true, title: _('Remove'), class: 'js-remove-tr btn btn-icon gl-button btn-danger gl-ml-3'
= paginate @broadcast_messages, theme: 'gitlab'
- if vue_app_enabled
#js-broadcast-messages{ data: { messages: @broadcast_messages.to_json } }
- else
= render 'form'
%br.clearfix
= render 'table'

View File

@ -137,7 +137,7 @@
.row.js-hide-when-nothing-matches-search
.col-lg-12
%hr
= f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3 js-password-prompt-btn'
= f.submit s_("Profiles|Update profile settings"), class: 'gl-mr-3 js-password-prompt-btn', pajamas_button: true
= link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel'
#password-prompt-modal

View File

@ -16,3 +16,4 @@
= f.submit _('Save changes'), class: "btn gl-button btn-confirm rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }
= render_if_exists 'projects/settings/merge_requests/merge_request_approvals_settings', expanded: true
= render_if_exists 'projects/settings/merge_requests/suggested_reviewers_settings', expanded: true

View File

@ -0,0 +1,8 @@
---
name: vue_broadcast_messages
introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98127"
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368847
milestone: '15.4'
type: development
group: group::optimize
default_enabled: false

View File

@ -379,6 +379,8 @@
- 5
- - process_commit
- 3
- - product_analytics_initialize_analytics
- 1
- - project_cache
- 1
- - project_destroy

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddJitsuTrackingColumnsToApplicationSettings < Gitlab::Database::Migration[2.0]
def change
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20220818125703_add_jitsu_tracking_columns_to_application_settings_text_limits.rb
add_column :application_settings, :jitsu_host, :text
add_column :application_settings, :jitsu_project_xid, :text
add_column :application_settings, :clickhouse_connection_string, :text
add_column :application_settings, :jitsu_administrator_email, :text
add_column :application_settings, :encrypted_jitsu_administrator_password, :binary
add_column :application_settings, :encrypted_jitsu_administrator_password_iv, :binary
# rubocop:enable Migration/AddLimitToTextColumns
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddJitsuTrackingColumnsToApplicationSettingsTextLimits < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
add_text_limit :application_settings, :jitsu_host, 255
add_text_limit :application_settings, :jitsu_project_xid, 255
add_text_limit :application_settings, :clickhouse_connection_string, 1024
add_text_limit :application_settings, :jitsu_administrator_email, 255
end
def down
remove_text_limit :application_settings, :jitsu_host
remove_text_limit :application_settings, :jitsu_project_xid
remove_text_limit :application_settings, :clickhouse_connection_string
remove_text_limit :application_settings, :jitsu_administrator_email
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddSuggestedReviewersEnabledToProjectSettings < Gitlab::Database::Migration[2.0]
enable_lock_retries!
def change
add_column :project_settings, :suggested_reviewers_enabled, :boolean, default: false, null: false
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
class AddSecureFilesMetadata < Gitlab::Database::Migration[2.0]
def change
add_column :ci_secure_files, :metadata, :jsonb
add_column :ci_secure_files, :expires_at, :datetime_with_timezone
end
end

View File

@ -0,0 +1 @@
ebcf446aa6579d93c57c2e96e8b670a43bcb6e20216f33a7f535e1bed50ace62

View File

@ -0,0 +1 @@
b60f36cd83174ce257baba4a74f0fcba6cd462fa2af6530ff5a3341536058e12

View File

@ -0,0 +1 @@
ff995d7a3c23959c4d4e6c6d0adfd338be36f6c07c98bacd26f282d84b2fa33d

View File

@ -0,0 +1 @@
2e20cfa3c1ebe77968ba923b381e0c95cb427613f2bfbed212ced4023bd4334e

View File

@ -11473,6 +11473,12 @@ CREATE TABLE application_settings (
cube_api_base_url text,
encrypted_cube_api_key bytea,
encrypted_cube_api_key_iv bytea,
jitsu_host text,
jitsu_project_xid text,
clickhouse_connection_string text,
jitsu_administrator_email text,
encrypted_jitsu_administrator_password bytea,
encrypted_jitsu_administrator_password_iv bytea,
dashboard_limit_enabled boolean DEFAULT false NOT NULL,
dashboard_limit integer DEFAULT 0 NOT NULL,
dashboard_notification_limit integer DEFAULT 0 NOT NULL,
@ -11514,12 +11520,16 @@ CREATE TABLE application_settings (
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
CONSTRAINT check_a5704163cc CHECK ((char_length(secret_detection_revocation_token_types_url) <= 255)),
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
CONSTRAINT check_d4865d70f3 CHECK ((char_length(clickhouse_connection_string) <= 1024)),
CONSTRAINT check_d820146492 CHECK ((char_length(spam_check_endpoint_url) <= 255)),
CONSTRAINT check_dea8792229 CHECK ((char_length(jitsu_host) <= 255)),
CONSTRAINT check_e2dd6e290a CHECK ((char_length(jira_connect_application_key) <= 255)),
CONSTRAINT check_e5024c8801 CHECK ((char_length(elasticsearch_username) <= 255)),
CONSTRAINT check_e5aba18f02 CHECK ((char_length(container_registry_version) <= 255)),
CONSTRAINT check_ec3ca9aa8d CHECK ((char_length(jitsu_administrator_email) <= 255)),
CONSTRAINT check_ef6176834f CHECK ((char_length(encrypted_cloud_license_auth_token_iv) <= 255)),
CONSTRAINT check_f6563bc000 CHECK ((char_length(arkose_labs_verify_api_url) <= 255))
CONSTRAINT check_f6563bc000 CHECK ((char_length(arkose_labs_verify_api_url) <= 255)),
CONSTRAINT check_fc732c181e CHECK ((char_length(jitsu_project_xid) <= 255))
);
COMMENT ON COLUMN application_settings.content_validation_endpoint_url IS 'JiHu-specific column';
@ -13357,6 +13367,8 @@ CREATE TABLE ci_secure_files (
file text NOT NULL,
checksum bytea NOT NULL,
key_data text,
metadata jsonb,
expires_at timestamp with time zone,
CONSTRAINT check_320790634d CHECK ((char_length(file) <= 255)),
CONSTRAINT check_402c7b4a56 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_7279b4e293 CHECK ((char_length(key_data) <= 128))
@ -20040,6 +20052,7 @@ CREATE TABLE project_settings (
enforce_auth_checks_on_uploads boolean DEFAULT true NOT NULL,
selective_code_owner_removals boolean DEFAULT false NOT NULL,
show_diff_preview_in_email boolean DEFAULT true NOT NULL,
suggested_reviewers_enabled boolean DEFAULT false NOT NULL,
CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)),
CONSTRAINT check_b09644994b CHECK ((char_length(squash_commit_template) <= 500)),
CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL)),

View File

@ -505,6 +505,7 @@ Parameters:
| `bio` | No | User's biography |
| `can_create_group` | No | User can create groups - true or false |
| `color_scheme_id` | No | User's color scheme for the file viewer (see [the user preference docs](../user/profile/preferences.md#syntax-highlighting-theme) for more information) |
| `commit_email` | No | User's commit email, `_private` to use the private commit email. |
| `email` | No | Email |
| `extern_uid` | No | External UID |
| `external` | No | Flags the user as external - true or false (default) |

View File

@ -69,6 +69,7 @@ as it can cause the pipeline to behave unexpectedly.
| `CI_JOB_JWT_V2` | 14.6 | all | A newly formatted RS256 JSON web token to increase compatibility. Similar to `CI_JOB_JWT`, except the issuer (`iss`) claim is changed from `gitlab.com` to `https://gitlab.com`, `sub` has changed from `job_id` to a string that contains the project path, and an `aud` claim is added. Format is subject to change. Be aware, the `aud` field is a constant value. Trusting JWTs in multiple relying parties can lead to [one RP sending a JWT to another one and acting maliciously as a job](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72555#note_769112331). **Note:** The `CI_JOB_JWT_V2` variable is available for testing, but the full feature is planned to be generally available when [issue 360657](https://gitlab.com/gitlab-org/gitlab/-/issues/360657) is complete.|
| `CI_JOB_MANUAL` | 8.12 | all | Only available if the job was started manually. `true` when available. |
| `CI_JOB_NAME` | 9.0 | 0.5 | The name of the job. |
| `CI_JOB_NAME_SLUG` | 15.4 | all | `CI_JOB_NAME_SLUG` in lowercase, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in paths. |
| `CI_JOB_STAGE` | 9.0 | 0.5 | The name of the job's stage. |
| `CI_JOB_STATUS` | all | 13.5 | The status of the job as each runner stage is executed. Use with [`after_script`](../yaml/index.md#after_script). Can be `success`, `failed`, or `canceled`. |
| `CI_JOB_TOKEN` | 9.0 | 1.2 | A token to authenticate with [certain API endpoints](../jobs/ci_job_token.md). The token is valid as long as the job is running. |

View File

@ -77,12 +77,14 @@ To create a release in the Releases page:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Deployments > Releases** and select **New release**.
1. From the [**Tag name**](release_fields.md#tag-name) dropdown, either:
1. From the [**Tag name**](release_fields.md#tag-name) dropdown list, either:
- Select an existing Git tag. Selecting an existing tag that is already associated with a release
results in a validation error.
- Enter a new Git tag name.
1. From the **Create from** dropdown, select a branch or commit SHA to use when creating the
new tag.
1. From the **Create from** dropdown list, select a branch or commit SHA to use when
creating the new tag.
1. Optional. In the **Set tag message** text box, enter a message to create an
[annotated tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging#_annotated_tags).
1. Optional. Enter additional information about the release, including:
- [Title](release_fields.md#title).
- [Milestones](#associate-milestones-with-a-release).

View File

@ -57,3 +57,5 @@ module API
end
end
end
API::Support::GitAccessActor.prepend_mod_with('API::Support::GitAccessActor')

View File

@ -51,6 +51,7 @@ module API
optional :bio, type: String, desc: 'The biography of the user'
optional :location, type: String, desc: 'The location of the user'
optional :public_email, type: String, desc: 'The public email of the user'
optional :commit_email, type: String, desc: 'The commit email, _private for private commit email'
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'

View File

@ -118,6 +118,7 @@ module Gitlab
def predefined_variables(job)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_JOB_NAME', value: job.name)
variables.append(key: 'CI_JOB_NAME_SLUG', value: job_name_slug(job))
variables.append(key: 'CI_JOB_STAGE', value: job.stage_name)
variables.append(key: 'CI_JOB_MANUAL', value: 'true') if job.action?
variables.append(key: 'CI_PIPELINE_TRIGGERED', value: 'true') if job.trigger_request
@ -145,6 +146,10 @@ module Gitlab
end
end
def job_name_slug(job)
job.name && Gitlab::Utils.slugify(job.name)
end
def ci_node_total_value(job)
parallel = job.options&.dig(:parallel)
parallel = parallel.dig(:total) if parallel.is_a?(Hash)

View File

@ -8,7 +8,7 @@ module Sidebars
def configure_menu_items
add_item(packages_registry_menu_item)
add_item(container_registry_menu_item)
add_item(harbor_registry__menu_item)
add_item(harbor_registry_menu_item)
add_item(dependency_proxy_menu_item)
true
end
@ -49,8 +49,10 @@ module Sidebars
)
end
def harbor_registry__menu_item
if Feature.disabled?(:harbor_registry_integration) || context.group.harbor_integration.nil?
def harbor_registry_menu_item
if Feature.disabled?(:harbor_registry_integration) ||
context.group.harbor_integration.nil? ||
!context.group.harbor_integration.activated?
return nil_menu_item(:harbor_registry)
end

View File

@ -9,7 +9,7 @@ module Sidebars
add_item(packages_registry_menu_item)
add_item(container_registry_menu_item)
add_item(infrastructure_registry_menu_item)
add_item(harbor_registry__menu_item)
add_item(harbor_registry_menu_item)
true
end
@ -65,8 +65,10 @@ module Sidebars
)
end
def harbor_registry__menu_item
if Feature.disabled?(:harbor_registry_integration, context.project) || context.project.harbor_integration.nil?
def harbor_registry_menu_item
if Feature.disabled?(:harbor_registry_integration, context.project) ||
context.project.harbor_integration.nil? ||
!context.project.harbor_integration.activated?
return ::Sidebars::NilMenuItem.new(item_id: :harbor_registry)
end

View File

@ -6039,6 +6039,9 @@ msgstr ""
msgid "Below you will find all the groups that are public."
msgstr ""
msgid "Beta"
msgstr ""
msgid "Bi-weekly code coverage"
msgstr ""
@ -11162,6 +11165,12 @@ msgstr ""
msgid "Create, update, or delete a merge request."
msgstr ""
msgid "CreateGitTag|Add a message to the tag. Leaving this blank creates a %{linkStart}lightweight tag%{linkEnd}."
msgstr ""
msgid "CreateGitTag|Set tag message"
msgstr ""
msgid "CreateGroup|You dont have permission to create a subgroup in this group."
msgstr ""
@ -31375,6 +31384,9 @@ msgstr ""
msgid "ProjectSettings|Enable merged results pipelines"
msgstr ""
msgid "ProjectSettings|Enable suggested reviewers"
msgstr ""
msgid "ProjectSettings|Encourage"
msgstr ""
@ -38801,6 +38813,15 @@ msgstr ""
msgid "SuggestedColors|Titanium yellow"
msgstr ""
msgid "SuggestedReviewers|Get suggestions for reviewers based on GitLab's machine learning tool."
msgstr ""
msgid "SuggestedReviewers|Suggested reviewers"
msgstr ""
msgid "SuggestedReviewers|Suggestions appear in the Reviewer section of the right sidebar"
msgstr ""
msgid "Suggestion is not applicable as the suggestion was not found."
msgstr ""

View File

@ -6,6 +6,7 @@ RSpec.describe 'Admin Broadcast Messages' do
before do
admin = create(:admin)
sign_in(admin)
stub_feature_flags(vue_broadcast_messages: false)
gitlab_enable_admin_mode_sign_in(admin)
create(
:broadcast_message,

View File

@ -11,6 +11,7 @@ RSpec.describe 'User creates release', :js do
let_it_be(:user) { create(:user) }
let(:new_page_url) { new_project_release_path(project) }
let(:tag_name) { 'new-tag' }
before do
project.add_developer(user)
@ -33,6 +34,8 @@ RSpec.describe 'User creates release', :js do
end
it 'defaults the "Create from" dropdown to the project\'s default branch' do
select_new_tag_name(tag_name)
expect(page.find('[data-testid="create-from-field"] .ref-selector button')).to have_content(project.default_branch)
end

View File

@ -4,6 +4,7 @@
"id",
"username",
"email",
"commit_email",
"name",
"state",
"avatar_url",

View File

@ -0,0 +1,35 @@
import { shallowMount } from '@vue/test-utils';
import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vue';
import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue';
import { MOCK_MESSAGES } from '../mock_data';
describe('BroadcastMessagesBase', () => {
let wrapper;
const findTable = () => wrapper.findComponent(MessagesTable);
function createComponent(props = {}) {
wrapper = shallowMount(BroadcastMessagesBase, {
propsData: {
messages: MOCK_MESSAGES,
...props,
},
});
}
afterEach(() => {
wrapper.destroy();
});
it('renders the table when there are existing messages', () => {
createComponent();
expect(findTable().exists()).toBe(true);
});
it('does not render the table when there are no existing messages', () => {
createComponent({ messages: [] });
expect(findTable().exists()).toBe(false);
});
});

View File

@ -0,0 +1,26 @@
import { shallowMount } from '@vue/test-utils';
import MessagesTableRow from '~/admin/broadcast_messages/components/messages_table_row.vue';
import { MOCK_MESSAGE } from '../mock_data';
describe('MessagesTableRow', () => {
let wrapper;
function createComponent(props = {}) {
wrapper = shallowMount(MessagesTableRow, {
propsData: {
message: MOCK_MESSAGE,
...props,
},
});
}
afterEach(() => {
wrapper.destroy();
});
it('renders the message ID', () => {
createComponent();
expect(wrapper.text()).toBe(`${MOCK_MESSAGE.id}`);
});
});

View File

@ -0,0 +1,29 @@
import { shallowMount } from '@vue/test-utils';
import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue';
import MessagesTableRow from '~/admin/broadcast_messages/components/messages_table_row.vue';
import { MOCK_MESSAGES } from '../mock_data';
describe('MessagesTable', () => {
let wrapper;
const findRows = () => wrapper.findAllComponents(MessagesTableRow);
function createComponent(props = {}) {
wrapper = shallowMount(MessagesTable, {
propsData: {
messages: MOCK_MESSAGES,
...props,
},
});
}
afterEach(() => {
wrapper.destroy();
});
it('renders a table row for each message', () => {
createComponent();
expect(findRows()).toHaveLength(MOCK_MESSAGES.length);
});
});

View File

@ -0,0 +1,5 @@
export const MOCK_MESSAGE = {
id: 100,
};
export const MOCK_MESSAGES = [MOCK_MESSAGE, { id: 200 }, { id: 300 }];

View File

@ -44,13 +44,6 @@ describe('WikiForm', () => {
await nextTick();
};
const dispatchBeforeUnload = () => {
const e = new Event('beforeunload');
jest.spyOn(e, 'preventDefault');
window.dispatchEvent(e);
return e;
};
const pageInfoNew = {
persisted: false,
uploadsPath: '/project/path/-/wikis/attachments',
@ -191,14 +184,6 @@ describe('WikiForm', () => {
expect(wrapper.text()).toContain(text);
});
it('starts with no unload warning', () => {
createWrapper();
const e = dispatchBeforeUnload();
expect(typeof e.returnValue).not.toBe('string');
expect(e.preventDefault).not.toHaveBeenCalled();
});
it.each`
persisted | titleHelpText | titleHelpLink
${true} | ${'You can move this page by adding the path to the beginning of the title.'} | ${'/help/user/project/wiki/index#move-a-wiki-page'}
@ -228,22 +213,11 @@ describe('WikiForm', () => {
await findMarkdownEditor().vm.$emit('input', ' Lorem ipsum dolar sit! ');
});
it('sets before unload warning', () => {
const e = dispatchBeforeUnload();
expect(e.preventDefault).toHaveBeenCalledTimes(1);
});
describe('form submit', () => {
beforeEach(async () => {
await triggerFormSubmit();
});
it('when form submitted, unsets before unload warning', () => {
const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
});
it('triggers wiki format tracking event', () => {
expect(trackingSpy).toHaveBeenCalledTimes(1);
});
@ -341,11 +315,6 @@ describe('WikiForm', () => {
await triggerFormSubmit();
});
it('unsets before unload warning on form submit', async () => {
const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
});
it('triggers tracking events on form submit', async () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,

View File

@ -1,14 +1,17 @@
import { GlDropdownItem } from '@gitlab/ui';
import { GlDropdownItem, GlFormGroup, GlSprintf } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores';
import createEditNewModule from '~/releases/stores/modules/edit_new';
const TEST_TAG_NAME = 'test-tag-name';
const TEST_TAG_MESSAGE = 'Test tag message';
const TEST_PROJECT_ID = '1234';
const TEST_CREATE_FROM = 'test-create-from';
const NONEXISTENT_TAG_NAME = 'nonexistent-tag';
@ -47,6 +50,8 @@ describe('releases/components/tag_field_new', () => {
store,
stubs: {
RefSelector: RefSelectorStub,
GlFormGroup,
GlSprintf,
},
});
};
@ -61,9 +66,11 @@ describe('releases/components/tag_field_new', () => {
});
store.state.editNew.createFrom = TEST_CREATE_FROM;
store.state.editNew.showCreateFrom = true;
store.state.editNew.release = {
tagName: TEST_TAG_NAME,
tagMessage: '',
assets: {
links: [],
},
@ -86,6 +93,9 @@ describe('releases/components/tag_field_new', () => {
const findCreateNewTagOption = () => wrapper.findComponent(GlDropdownItem);
const findAnnotatedTagMessageFormGroup = () =>
wrapper.find('[data-testid="annotated-tag-message-field"]');
describe('"Tag name" field', () => {
describe('rendering and behavior', () => {
beforeEach(() => createComponent());
@ -124,6 +134,10 @@ describe('releases/components/tag_field_new', () => {
expect(findCreateFromFormGroup().exists()).toBe(false);
});
it('hides the "Tag message" field', () => {
expect(findAnnotatedTagMessageFormGroup().exists()).toBe(false);
});
it('fetches the release notes for the tag', () => {
const expectedUrl = `/api/v4/projects/1234/repository/tags/${updatedTagName}`;
expect(mock.history.get).toContainEqual(expect.objectContaining({ url: expectedUrl }));
@ -230,4 +244,34 @@ describe('releases/components/tag_field_new', () => {
});
});
});
describe('"Annotated Tag" field', () => {
beforeEach(() => {
createComponent(mountExtended);
});
it('renders a label', () => {
expect(wrapper.findByRole('textbox', { name: 'Set tag message' }).exists()).toBe(true);
});
it('renders a description', () => {
expect(trimText(findAnnotatedTagMessageFormGroup().text())).toContain(
'Add a message to the tag. Leaving this blank creates a lightweight tag.',
);
});
it('updates the store', async () => {
await findAnnotatedTagMessageFormGroup().find('textarea').setValue(TEST_TAG_MESSAGE);
expect(store.state.editNew.release.tagMessage).toBe(TEST_TAG_MESSAGE);
});
it('shows a link', () => {
const link = wrapper.findByRole('link', {
name: 'lightweight tag',
});
expect(link.attributes('href')).toBe('https://git-scm.com/book/en/v2/Git-Basics-Tagging/');
});
});
});

View File

@ -169,6 +169,15 @@ describe('Release edit/new actions', () => {
});
});
describe('updateReleaseTagMessage', () => {
it(`commits ${types.UPDATE_RELEASE_TAG_MESSAGE} with the updated tag name`, () => {
const newMessage = 'updated-tag-message';
return testAction(actions.updateReleaseTagMessage, newMessage, state, [
{ type: types.UPDATE_RELEASE_TAG_MESSAGE, payload: newMessage },
]);
});
});
describe('updateReleasedAt', () => {
it(`commits ${types.UPDATE_RELEASED_AT} with the updated date`, () => {
const newDate = new Date();

View File

@ -332,6 +332,7 @@ describe('Release edit/new getters', () => {
it('returns all the data needed for the releaseCreate GraphQL query', () => {
const state = {
createFrom: 'main',
release: { tagMessage: 'hello' },
};
const otherGetters = {
@ -352,6 +353,7 @@ describe('Release edit/new getters', () => {
const expectedVariables = {
input: {
name: 'release.name',
tagMessage: 'hello',
ref: 'main',
assets: {
links: [

View File

@ -26,6 +26,7 @@ describe('Release edit/new mutations', () => {
expect(state.release).toEqual({
tagName: 'v1.3',
tagMessage: '',
name: '',
description: '',
milestones: [],
@ -90,6 +91,16 @@ describe('Release edit/new mutations', () => {
});
});
describe(`${types.UPDATE_RELEASE_TAG_MESSAGE}`, () => {
it("updates the release's tag message", () => {
state.release = release;
const newMessage = 'updated-tag-message';
mutations[types.UPDATE_RELEASE_TAG_MESSAGE](state, newMessage);
expect(state.release.tagMessage).toBe(newMessage);
});
});
describe(`${types.UPDATE_RELEASED_AT}`, () => {
it("updates the release's released at date", () => {
state.release = release;

View File

@ -10,6 +10,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:job) do
create(:ci_build,
name: 'rspec:test 1',
pipeline: pipeline,
user: user,
yaml_variables: [{ key: 'YAML_VARIABLE', value: 'value' }]
@ -24,13 +25,15 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do
let(:predefined_variables) do
[
{ key: 'CI_JOB_NAME',
value: job.name },
value: 'rspec:test 1' },
{ key: 'CI_JOB_NAME_SLUG',
value: 'rspec-test-1' },
{ key: 'CI_JOB_STAGE',
value: job.stage_name },
{ key: 'CI_NODE_TOTAL',
value: '1' },
{ key: 'CI_BUILD_NAME',
value: job.name },
value: 'rspec:test 1' },
{ key: 'CI_BUILD_STAGE',
value: job.stage_name },
{ key: 'CI',

View File

@ -207,6 +207,16 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
it_behaves_like 'the menu entry is available'
end
context 'when config harbor registry setting is not activated' do
before do
harbor_integration.update!(active: false)
end
let(:harbor_registry_enabled) { true }
it_behaves_like 'the menu entry is not available'
end
end
end

View File

@ -166,6 +166,15 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
is_expected.not_to be_nil
end
end
context 'when config harbor registry setting is not activated' do
it 'does not add the menu item to the list' do
stub_feature_flags(harbor_registry_integration: true)
project.harbor_integration.update!(active: false)
is_expected.to be_nil
end
end
end
end
end

View File

@ -86,9 +86,9 @@ RSpec.describe Ci::Bridge do
describe '#scoped_variables' do
it 'returns a hash representing variables' do
variables = %w[
CI_JOB_NAME CI_JOB_STAGE CI_COMMIT_SHA CI_COMMIT_SHORT_SHA
CI_COMMIT_BEFORE_SHA CI_COMMIT_REF_NAME CI_COMMIT_REF_SLUG
CI_PROJECT_ID CI_PROJECT_NAME CI_PROJECT_PATH
CI_JOB_NAME CI_JOB_NAME_SLUG CI_JOB_STAGE CI_COMMIT_SHA
CI_COMMIT_SHORT_SHA CI_COMMIT_BEFORE_SHA CI_COMMIT_REF_NAME
CI_COMMIT_REF_SLUG CI_PROJECT_ID CI_PROJECT_NAME CI_PROJECT_PATH
CI_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PROJECT_ROOT_NAMESPACE
CI_PIPELINE_IID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE
CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED

View File

@ -2668,6 +2668,7 @@ RSpec.describe Ci::Build do
{ key: 'CI_JOB_JWT_V1', value: 'ci.job.jwt', public: false, masked: true },
{ key: 'CI_JOB_JWT_V2', value: 'ci.job.jwtv2', public: false, masked: true },
{ key: 'CI_JOB_NAME', value: 'test', public: true, masked: false },
{ key: 'CI_JOB_NAME_SLUG', value: 'test', public: true, masked: false },
{ key: 'CI_JOB_STAGE', value: 'test', public: true, masked: false },
{ key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false },
{ key: 'CI_BUILD_NAME', value: 'test', public: true, masked: false },

View File

@ -101,10 +101,21 @@ RSpec.describe ProjectAuthorization do
end
before do
stub_const("#{described_class.name}::BATCH_SIZE", per_batch_size)
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end
shared_examples_for 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch' do
specify do
expect(described_class).not_to receive(:sleep)
described_class.insert_all_in_batches(attributes, per_batch_size)
expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values))
end
end
context 'when the total number of records to be inserted is greater than the batch size' do
let(:per_batch_size) { 2 }
@ -116,19 +127,21 @@ RSpec.describe ProjectAuthorization do
expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values))
end
context 'when the GitLab installation does not have a replica database configured' do
before do
# Configure as if a replica database is not enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(true)
end
it_behaves_like 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch'
end
end
context 'when the total number of records to be inserted is less than the batch size' do
let(:per_batch_size) { 5 }
it 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch' do
expect(described_class).to receive(:insert_all).once.and_call_original
expect(described_class).not_to receive(:sleep)
described_class.insert_all_in_batches(attributes, per_batch_size)
expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values))
end
it_behaves_like 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch'
end
end
@ -142,7 +155,8 @@ RSpec.describe ProjectAuthorization do
let(:user_ids) { [user_1.id, user_2.id, user_3.id] }
before do
stub_const("#{described_class.name}::BATCH_SIZE", per_batch_size)
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end
@ -153,6 +167,20 @@ RSpec.describe ProjectAuthorization do
create(:project_authorization, user: user_4, project: project)
end
shared_examples_for 'removes the project authorizations of the specified users in the current project, without a delay between each batch' do
specify do
expect(described_class).not_to receive(:sleep)
described_class.delete_all_in_batches_for_project(
project: project,
user_ids: user_ids,
per_batch: per_batch_size
)
expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids)
end
end
context 'when the total number of records to be removed is greater than the batch size' do
let(:per_batch_size) { 2 }
@ -167,22 +195,21 @@ RSpec.describe ProjectAuthorization do
expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids)
end
context 'when the GitLab installation does not have a replica database configured' do
before do
# Configure as if a replica database is not enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(true)
end
it_behaves_like 'removes the project authorizations of the specified users in the current project, without a delay between each batch'
end
end
context 'when the total number of records to be removed is less than the batch size' do
let(:per_batch_size) { 5 }
it 'removes the project authorizations of the specified users in the current project, without a delay between each batch' do
expect(described_class).not_to receive(:sleep)
described_class.delete_all_in_batches_for_project(
project: project,
user_ids: user_ids,
per_batch: per_batch_size
)
expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids)
end
it_behaves_like 'removes the project authorizations of the specified users in the current project, without a delay between each batch'
end
end
@ -196,7 +223,8 @@ RSpec.describe ProjectAuthorization do
let(:project_ids) { [project_1.id, project_2.id, project_3.id] }
before do
stub_const("#{described_class.name}::BATCH_SIZE", per_batch_size)
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end
@ -207,26 +235,8 @@ RSpec.describe ProjectAuthorization do
create(:project_authorization, user: user, project: project_4)
end
context 'when the total number of records to be removed is greater than the batch size' do
let(:per_batch_size) { 2 }
it 'removes the project authorizations of the specified users in the current project, with a delay between each batch' do
expect(described_class).to receive(:sleep).twice
described_class.delete_all_in_batches_for_user(
user: user,
project_ids: project_ids,
per_batch: per_batch_size
)
expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids)
end
end
context 'when the total number of records to be removed is less than the batch size' do
let(:per_batch_size) { 5 }
it 'removes the project authorizations of the specified users in the current project, without a delay between each batch' do
shared_examples_for 'removes the project authorizations of the specified projects from the current user, without a delay between each batch' do
specify do
expect(described_class).not_to receive(:sleep)
described_class.delete_all_in_batches_for_user(
@ -238,5 +248,36 @@ RSpec.describe ProjectAuthorization do
expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids)
end
end
context 'when the total number of records to be removed is greater than the batch size' do
let(:per_batch_size) { 2 }
it 'removes the project authorizations of the specified projects from the current user, with a delay between each batch' do
expect(described_class).to receive(:sleep).twice
described_class.delete_all_in_batches_for_user(
user: user,
project_ids: project_ids,
per_batch: per_batch_size
)
expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids)
end
context 'when the GitLab installation does not have a replica database configured' do
before do
# Configure as if a replica database is not enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(true)
end
it_behaves_like 'removes the project authorizations of the specified projects from the current user, without a delay between each batch'
end
end
context 'when the total number of records to be removed is less than the batch size' do
let(:per_batch_size) { 5 }
it_behaves_like 'removes the project authorizations of the specified projects from the current user, without a delay between each batch'
end
end
end

View File

@ -21,6 +21,10 @@ RSpec.describe ProjectSetting, type: :model do
it { is_expected.not_to allow_value(nil).for(:target_platforms) }
it { is_expected.to allow_value([]).for(:target_platforms) }
it { is_expected.not_to allow_value(nil).for(:suggested_reviewers_enabled) }
it { is_expected.to allow_value(true).for(:suggested_reviewers_enabled) }
it { is_expected.to allow_value(false).for(:suggested_reviewers_enabled) }
it 'allows any combination of the allowed target platforms' do
valid_target_platform_combinations.each do |target_platforms|
expect(subject).to allow_value(target_platforms).for(:target_platforms)

View File

@ -161,6 +161,7 @@ project_setting:
- target_platforms
- selective_code_owner_removals
- show_diff_preview_in_email
- suggested_reviewers_enabled
build_service_desk_setting: # service_desk_setting
unexposed_attributes:

View File

@ -4,15 +4,29 @@ require 'spec_helper'
RSpec.describe GoogleCloud::EnableCloudsqlService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:params) do
{
google_oauth2_token: 'mock-token',
gcp_project_id: 'mock-gcp-project-id',
environment_name: 'main'
}
end
subject(:result) { described_class.new(project).execute }
subject(:result) { described_class.new(project, user, params).execute }
context 'when a project does not have any GCP_PROJECT_IDs configured' do
it 'returns error' do
message = 'No GCP projects found. Configure a service account or GCP_PROJECT_ID CI variable.'
it 'creates GCP_PROJECT_ID project var' do
expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
expect(instance).to receive(:enable_cloud_sql_admin).with('mock-gcp-project-id')
expect(instance).to receive(:enable_compute).with('mock-gcp-project-id')
expect(instance).to receive(:enable_service_networking).with('mock-gcp-project-id')
end
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq(message)
expect(result[:status]).to eq(:success)
expect(project.variables.count).to eq(1)
expect(project.variables.first.key).to eq('GCP_PROJECT_ID')
expect(project.variables.first.value).to eq('mock-gcp-project-id')
end
end
@ -30,6 +44,9 @@ RSpec.describe GoogleCloud::EnableCloudsqlService do
it 'enables cloudsql, compute and service networking Google APIs', :aggregate_failures do
expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
expect(instance).to receive(:enable_cloud_sql_admin).with('mock-gcp-project-id')
expect(instance).to receive(:enable_compute).with('mock-gcp-project-id')
expect(instance).to receive(:enable_service_networking).with('mock-gcp-project-id')
expect(instance).to receive(:enable_cloud_sql_admin).with('prj-prod')
expect(instance).to receive(:enable_compute).with('prj-prod')
expect(instance).to receive(:enable_service_networking).with('prj-prod')
@ -44,6 +61,9 @@ RSpec.describe GoogleCloud::EnableCloudsqlService do
context 'when Google APIs raise an error' do
it 'returns error result' do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance|
allow(instance).to receive(:enable_cloud_sql_admin).with('mock-gcp-project-id')
allow(instance).to receive(:enable_compute).with('mock-gcp-project-id')
allow(instance).to receive(:enable_service_networking).with('mock-gcp-project-id')
allow(instance).to receive(:enable_cloud_sql_admin).with('prj-prod')
allow(instance).to receive(:enable_compute).with('prj-prod')
allow(instance).to receive(:enable_service_networking).with('prj-prod')

View File

@ -35,6 +35,34 @@ RSpec.shared_examples 'edits content using the content editor' do
attach_file('content_editor_image', Rails.root.join('spec', 'fixtures', fixture_name), make_visible: true)
end
def wait_until_hidden_field_is_updated(value)
expect(page).to have_field('wiki[content]', with: value, type: 'hidden')
end
it 'saves page content in local storage if the user navigates away' do
switch_to_content_editor
expect(page).to have_css(content_editor_testid)
type_in_content_editor ' Typing text in the content editor'
wait_until_hidden_field_is_updated /Typing text in the content editor/
refresh
expect(page).to have_text('Typing text in the content editor')
refresh # also retained after second refresh
expect(page).to have_text('Typing text in the content editor')
click_link 'Cancel' # draft is deleted on cancel
page.go_back
expect(page).not_to have_text('Typing text in the content editor')
end
describe 'formatting bubble menu' do
it 'shows a formatting bubble menu for a regular paragraph and headings' do
switch_to_content_editor

View File

@ -147,6 +147,18 @@ RSpec.shared_examples 'User creates wiki page' do
end
end
it 'saves page content in local storage if the user navigates away', :js do
fill_in(:wiki_title, with: "Test title")
fill_in(:wiki_content, with: "This is a test")
fill_in(:wiki_message, with: "Test commit message")
refresh
expect(page).to have_field(:wiki_title, with: "Test title")
expect(page).to have_field(:wiki_content, with: "This is a test")
expect(page).to have_field(:wiki_message, with: "Test commit message")
end
it 'creates a wiki page with Org markup', :aggregate_failures, :js do
org_content = <<~ORG
* Heading

View File

@ -78,6 +78,18 @@ RSpec.shared_examples 'User updates wiki page' do
expect(page).to have_content('My awesome wiki!')
end
it 'saves page content in local storage if the user navigates away', :js do
fill_in(:wiki_title, with: "Test title")
fill_in(:wiki_content, with: "This is a test")
fill_in(:wiki_message, with: "Test commit message")
refresh
expect(page).to have_field(:wiki_title, with: "Test title")
expect(page).to have_field(:wiki_content, with: "This is a test")
expect(page).to have_field(:wiki_message, with: "Test commit message")
end
it 'updates the commit message as the title is changed', :js do
fill_in(:wiki_title, with: '& < > \ \ { } &')

View File

@ -18,6 +18,7 @@ RSpec.shared_examples 'requires valid Google Oauth2 token' do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true)
allow(client).to receive(:list_projects).and_return(mock_gcp_projects) if mock_gcp_projects
allow(client).to receive(:create_cloudsql_instance)
end
allow_next_instance_of(BranchesFinder) do |finder|

View File

@ -3,20 +3,22 @@
require 'spec_helper'
RSpec.describe 'admin/broadcast_messages/index' do
let(:role_targeted_broadcast_messages) { true }
let(:vue_broadcast_messages) { false }
let_it_be(:message) { create(:broadcast_message, broadcast_type: 'banner', target_access_levels: [Gitlab::Access::GUEST, Gitlab::Access::DEVELOPER]) }
before do
assign(:broadcast_messages, BroadcastMessage.page(1))
assign(:broadcast_message, BroadcastMessage.new)
stub_feature_flags(role_targeted_broadcast_messages: role_targeted_broadcast_messages)
stub_feature_flags(vue_broadcast_messages: vue_broadcast_messages)
render
end
describe 'Target roles select and table column' do
let(:feature_flag_state) { true }
let_it_be(:message) { create(:broadcast_message, broadcast_type: 'banner', target_access_levels: [Gitlab::Access::GUEST, Gitlab::Access::DEVELOPER]) }
before do
assign(:broadcast_messages, BroadcastMessage.page(1))
assign(:broadcast_message, BroadcastMessage.new)
stub_feature_flags(role_targeted_broadcast_messages: feature_flag_state)
render
end
it 'rendered' do
expect(rendered).to have_content('Target roles')
expect(rendered).to have_content('Owner')
@ -24,7 +26,7 @@ RSpec.describe 'admin/broadcast_messages/index' do
end
context 'when feature flag is off' do
let(:feature_flag_state) { false }
let(:role_targeted_broadcast_messages) { false }
it 'is not rendered' do
expect(rendered).not_to have_content('Target roles')
@ -33,4 +35,18 @@ RSpec.describe 'admin/broadcast_messages/index' do
end
end
end
describe 'Vue application' do
it 'is not rendered' do
expect(rendered).not_to have_selector('#js-broadcast-messages')
end
context 'when feature flag is on' do
let(:vue_broadcast_messages) { true }
it 'is rendered' do
expect(rendered).to have_selector('#js-broadcast-messages')
end
end
end
end

View File

@ -1 +1 @@
golang 1.17.9
golang 1.18.6