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: variables:
PG_VERSION: "12" 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" RAILS_ENV: "test"
NODE_ENV: "test" NODE_ENV: "test"
BUNDLE_WITHOUT: "production:development" BUNDLE_WITHOUT: "production:development"
@ -80,6 +80,7 @@ variables:
CHROME_VERSION: "103" CHROME_VERSION: "103"
DOCKER_VERSION: "20.10.14" DOCKER_VERSION: "20.10.14"
RUBY_VERSION: "2.7" RUBY_VERSION: "2.7"
GO_VERSION: "1.18"
TMP_TEST_FOLDER: "${CI_PROJECT_DIR}/tmp/tests" TMP_TEST_FOLDER: "${CI_PROJECT_DIR}/tmp/tests"
GITLAB_WORKHORSE_FOLDER: "gitlab-workhorse" GITLAB_WORKHORSE_FOLDER: "gitlab-workhorse"

View File

@ -1,6 +1,6 @@
workhorse:verify: workhorse:verify:
extends: .workhorse:rules:workhorse extends: .workhorse:rules:workhorse
image: ${GITLAB_DEPENDENCY_PROXY}golang:1.17 image: ${GITLAB_DEPENDENCY_PROXY}golang:1.18
stage: test stage: test
needs: [] needs: []
script: 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 image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}-golang-${GO_VERSION}:git-2.36
variables: variables:
GITALY_ADDRESS: "tcp://127.0.0.1:8075" GITALY_ADDRESS: "tcp://127.0.0.1:8075"
GO_VERSION: "1.17" GO_VERSION: "1.18"
stage: test stage: test
needs: needs:
- setup-test-env - 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 initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
import initBroadcastMessagesForm from './broadcast_message'; import initBroadcastMessagesForm from './broadcast_message';
if (gon.features.vueBroadcastMessages) {
initBroadcastMessages();
} else {
initBroadcastMessagesForm(); initBroadcastMessagesForm();
initDeprecatedRemoveRowBehavior(); initDeprecatedRemoveRowBehavior();
}

View File

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

View File

@ -1,8 +1,15 @@
<script> <script>
import { GlFormGroup, GlDropdownItem, GlSprintf } from '@gitlab/ui'; import {
GlCollapse,
GlLink,
GlFormGroup,
GlFormTextarea,
GlDropdownItem,
GlSprintf,
} from '@gitlab/ui';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { mapState, mapActions, mapGetters } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import { __ } from '~/locale'; import { __, s__ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue'; import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_TAGS } from '~/ref/constants'; import { REF_TYPE_TAGS } from '~/ref/constants';
import FormFieldContainer from './form_field_container.vue'; import FormFieldContainer from './form_field_container.vue';
@ -10,7 +17,10 @@ import FormFieldContainer from './form_field_container.vue';
export default { export default {
name: 'TagFieldNew', name: 'TagFieldNew',
components: { components: {
GlCollapse,
GlFormGroup, GlFormGroup,
GlFormTextarea,
GlLink,
RefSelector, RefSelector,
FormFieldContainer, FormFieldContainer,
GlDropdownItem, GlDropdownItem,
@ -41,6 +51,14 @@ export default {
this.updateShowCreateFrom(false); this.updateShowCreateFrom(false);
}, },
}, },
tagMessage: {
get() {
return this.release.tagMessage;
},
set(tagMessage) {
this.updateReleaseTagMessage(tagMessage);
},
},
createFromModel: { createFromModel: {
get() { get() {
return this.createFrom; return this.createFrom;
@ -70,6 +88,7 @@ export default {
methods: { methods: {
...mapActions('editNew', [ ...mapActions('editNew', [
'updateReleaseTagName', 'updateReleaseTagName',
'updateReleaseTagMessage',
'updateCreateFrom', 'updateCreateFrom',
'fetchTagNotes', 'fetchTagNotes',
'updateShowCreateFrom', 'updateShowCreateFrom',
@ -113,9 +132,20 @@ export default {
noRefSelected: __('No source selected'), noRefSelected: __('No source selected'),
searchPlaceholder: __('Search branches, tags, and commits'), searchPlaceholder: __('Search branches, tags, and commits'),
dropdownHeader: __('Select source'), 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], tagNameEnabledRefTypes: [REF_TYPE_TAGS],
gitTagDocsLink: 'https://git-scm.com/book/en/v2/Git-Basics-Tagging/',
}; };
</script> </script>
<template> <template>
@ -156,9 +186,11 @@ export default {
</ref-selector> </ref-selector>
</form-field-container> </form-field-container>
</gl-form-group> </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 <gl-form-group
v-if="showCreateFrom" v-if="showCreateFrom"
:label="__('Create from')" :label="$options.translations.createFrom.label"
:label-for="createFromSelectorId" :label-for="createFromSelectorId"
data-testid="create-from-field" data-testid="create-from-field"
> >
@ -170,9 +202,29 @@ export default {
:translations="$options.translations.createFrom" :translations="$options.translations.createFrom"
/> />
</form-field-container> </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> <template #description>
{{ __('Existing branch name, tag, or commit SHA') }} <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> </template>
</gl-form-group> </gl-form-group>
</div> </div>
</gl-collapse>
</div>
</template> </template>

View File

@ -57,6 +57,9 @@ export const fetchRelease = async ({ commit, state }) => {
export const updateReleaseTagName = ({ commit }, tagName) => export const updateReleaseTagName = ({ commit }, tagName) =>
commit(types.UPDATE_RELEASE_TAG_NAME, 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) => export const updateCreateFrom = ({ commit }, createFrom) =>
commit(types.UPDATE_CREATE_FROM, createFrom); commit(types.UPDATE_CREATE_FROM, createFrom);

View File

@ -145,6 +145,7 @@ export const releaseCreateMutatationVariables = (state, getters) => {
input: { input: {
...getters.releaseUpdateMutatationVariables.input, ...getters.releaseUpdateMutatationVariables.input,
ref: state.createFrom, ref: state.createFrom,
tagMessage: state.release.tagMessage,
assets: { assets: {
links: getters.releaseLinksToCreate.map(({ name, url, linkType }) => ({ links: getters.releaseLinksToCreate.map(({ name, url, linkType }) => ({
name: name.trim(), 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 RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
export const UPDATE_RELEASE_TAG_NAME = 'UPDATE_RELEASE_TAG_NAME'; 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_CREATE_FROM = 'UPDATE_CREATE_FROM';
export const UPDATE_SHOW_CREATE_FROM = 'UPDATE_SHOW_CREATE_FROM'; export const UPDATE_SHOW_CREATE_FROM = 'UPDATE_SHOW_CREATE_FROM';
export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE'; export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';

View File

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

View File

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

View File

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

View File

@ -50,16 +50,15 @@ module Projects
track_event(:error_enable_cloudsql_services) track_event(:error_enable_cloudsql_services)
flash[:error] = error_message(enable_response[:message]) flash[:error] = error_message(enable_response[:message])
else else
permitted_params = params.permit(:gcp_project, :ref, :database_version, :tier)
create_response = ::GoogleCloud::CreateCloudsqlInstanceService create_response = ::GoogleCloud::CreateCloudsqlInstanceService
.new(project, current_user, create_service_params(permitted_params)) .new(project, current_user, create_service_params)
.execute .execute
if create_response[:status] == :error if create_response[:status] == :error
track_event(:error_create_cloudsql_instance) track_event(:error_create_cloudsql_instance)
flash[:warning] = error_message(create_response[:message]) flash[:warning] = error_message(create_response[:message])
else else
track_event(:create_cloudsql_instance, permitted_params.to_s) track_event(:create_cloudsql_instance, permitted_params_create.to_s)
flash[:notice] = success_message flash[:notice] = success_message
end end
end end
@ -69,17 +68,25 @@ module Projects
private private
def enable_service_params def permitted_params_create
{ google_oauth2_token: token_in_session } params.permit(:gcp_project, :ref, :database_version, :tier)
end end
def create_service_params(permitted_params) def enable_service_params
{ {
google_oauth2_token: token_in_session, google_oauth2_token: token_in_session,
gcp_project_id: permitted_params[:gcp_project], gcp_project_id: permitted_params_create[:gcp_project],
environment_name: permitted_params[:ref], environment_name: permitted_params_create[:ref]
database_version: permitted_params[:database_version], }
tier: permitted_params[:tier] 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 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_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 :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 :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, validates :disable_feed_token,
inclusion: { in: [true, false], message: _('must be a boolean value') } 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 :file, presence: true, file_size: { maximum: FILE_SIZE_LIMIT }
validates :checksum, :file_store, :name, :project_id, presence: true validates :checksum, :file_store, :name, :project_id, presence: true
validates :name, uniqueness: { scope: :project } validates :name, uniqueness: { scope: :project }
validates :metadata, json_schema: { filename: "ci_secure_file_metadata" }, allow_nil: true
after_initialize :generate_key_data after_initialize :generate_key_data
before_validation :assign_checksum before_validation :assign_checksum
@ -23,6 +24,8 @@ module Ci
scope :order_by_created_at, -> { order(created_at: :desc) } scope :order_by_created_at, -> { order(created_at: :desc) }
scope :project_id_in, ->(ids) { where(project_id: ids) } 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 } default_value_for(:file_store) { Ci::SecureFileUploader.default_store }
mount_file_store_uploader Ci::SecureFileUploader mount_file_store_uploader Ci::SecureFileUploader

View File

@ -2,6 +2,8 @@
class ProjectAuthorization < ApplicationRecord class ProjectAuthorization < ApplicationRecord
BATCH_SIZE = 1000 BATCH_SIZE = 1000
SLEEP_DELAY = 0.1
extend SuppressCompositePrimaryKeyWarning extend SuppressCompositePrimaryKeyWarning
include FromUnion include FromUnion
@ -55,12 +57,16 @@ class ProjectAuthorization < ApplicationRecord
end end
private_class_method def self.add_delay_between_batches?(entire_size:, batch_size:) 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 && entire_size > batch_size &&
!::Gitlab::Database::LoadBalancing.primary_only? &&
Feature.enabled?(:enable_minor_delay_during_project_authorizations_refresh) Feature.enabled?(:enable_minor_delay_during_project_authorizations_refresh)
end end
private_class_method def self.perform_delay private_class_method def self.perform_delay
sleep(0.1) sleep(SLEEP_DELAY)
end end
end end

View File

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

View File

@ -3,7 +3,7 @@
module GoogleCloud module GoogleCloud
class EnableCloudsqlService < ::GoogleCloud::BaseService class EnableCloudsqlService < ::GoogleCloud::BaseService
def execute 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| unique_gcp_project_ids.each do |gcp_project_id|
google_api_client.enable_cloud_sql_admin(gcp_project_id) google_api_client.enable_cloud_sql_admin(gcp_project_id)
@ -18,8 +18,8 @@ module GoogleCloud
private private
def no_projects_error def ci_var_protected?
error("No GCP projects found. Configure a service account or GCP_PROJECT_ID CI variable.") ProtectedBranch.protected?(project, environment_name) || ProtectedTag.protected?(project, environment_name)
end end
end 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 .form-text.text-muted
= _('Only required if not using role instance credentials.') = _('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 .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.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.') = 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') } - 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 } = 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 } - 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.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") - breadcrumb_title _("Messages")
- page_title _("Broadcast 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 %h1.page-title.gl-font-size-h-display
= _('Broadcast Messages') = _('Broadcast Messages')
%p.light %p.light
= _('Use banners and notifications to notify your users about scheduled maintenance, recent upgrades, and more.') = _('Use banners and notifications to notify your users about scheduled maintenance, recent upgrades, and more.')
- if vue_app_enabled
#js-broadcast-messages{ data: { messages: @broadcast_messages.to_json } }
- else
= render 'form' = render 'form'
%br.clearfix %br.clearfix
= render 'table'
- 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

@ -137,7 +137,7 @@
.row.js-hide-when-nothing-matches-search .row.js-hide-when-nothing-matches-search
.col-lg-12 .col-lg-12
%hr %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' = link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel'
#password-prompt-modal #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' } = 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/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 - 5
- - process_commit - - process_commit
- 3 - 3
- - product_analytics_initialize_analytics
- 1
- - project_cache - - project_cache
- 1 - 1
- - project_destroy - - 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, cube_api_base_url text,
encrypted_cube_api_key bytea, encrypted_cube_api_key bytea,
encrypted_cube_api_key_iv 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_enabled boolean DEFAULT false NOT NULL,
dashboard_limit integer DEFAULT 0 NOT NULL, dashboard_limit integer DEFAULT 0 NOT NULL,
dashboard_notification_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_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
CONSTRAINT check_a5704163cc CHECK ((char_length(secret_detection_revocation_token_types_url) <= 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_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_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_e2dd6e290a CHECK ((char_length(jira_connect_application_key) <= 255)),
CONSTRAINT check_e5024c8801 CHECK ((char_length(elasticsearch_username) <= 255)), CONSTRAINT check_e5024c8801 CHECK ((char_length(elasticsearch_username) <= 255)),
CONSTRAINT check_e5aba18f02 CHECK ((char_length(container_registry_version) <= 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_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'; 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, file text NOT NULL,
checksum bytea NOT NULL, checksum bytea NOT NULL,
key_data text, key_data text,
metadata jsonb,
expires_at timestamp with time zone,
CONSTRAINT check_320790634d CHECK ((char_length(file) <= 255)), CONSTRAINT check_320790634d CHECK ((char_length(file) <= 255)),
CONSTRAINT check_402c7b4a56 CHECK ((char_length(name) <= 255)), CONSTRAINT check_402c7b4a56 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_7279b4e293 CHECK ((char_length(key_data) <= 128)) 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, enforce_auth_checks_on_uploads boolean DEFAULT true NOT NULL,
selective_code_owner_removals boolean DEFAULT false NOT NULL, selective_code_owner_removals boolean DEFAULT false NOT NULL,
show_diff_preview_in_email boolean DEFAULT true 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_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)),
CONSTRAINT check_b09644994b CHECK ((char_length(squash_commit_template) <= 500)), CONSTRAINT check_b09644994b CHECK ((char_length(squash_commit_template) <= 500)),
CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL)), CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL)),

View File

@ -505,6 +505,7 @@ Parameters:
| `bio` | No | User's biography | | `bio` | No | User's biography |
| `can_create_group` | No | User can create groups - true or false | | `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) | | `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 | | `email` | No | Email |
| `extern_uid` | No | External UID | | `extern_uid` | No | External UID |
| `external` | No | Flags the user as external - true or false (default) | | `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_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_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` | 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_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_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. | | `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 top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Deployments > Releases** and select **New release**. 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 - Select an existing Git tag. Selecting an existing tag that is already associated with a release
results in a validation error. results in a validation error.
- Enter a new Git tag name. - Enter a new Git tag name.
1. From the **Create from** dropdown, select a branch or commit SHA to use when creating the 1. From the **Create from** dropdown list, select a branch or commit SHA to use when
new tag. 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: 1. Optional. Enter additional information about the release, including:
- [Title](release_fields.md#title). - [Title](release_fields.md#title).
- [Milestones](#associate-milestones-with-a-release). - [Milestones](#associate-milestones-with-a-release).

View File

@ -57,3 +57,5 @@ module API
end end
end 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 :bio, type: String, desc: 'The biography of the user'
optional :location, type: String, desc: 'The location 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 :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 :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 :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' 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) def predefined_variables(job)
Gitlab::Ci::Variables::Collection.new.tap do |variables| Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_JOB_NAME', value: job.name) 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_STAGE', value: job.stage_name)
variables.append(key: 'CI_JOB_MANUAL', value: 'true') if job.action? variables.append(key: 'CI_JOB_MANUAL', value: 'true') if job.action?
variables.append(key: 'CI_PIPELINE_TRIGGERED', value: 'true') if job.trigger_request variables.append(key: 'CI_PIPELINE_TRIGGERED', value: 'true') if job.trigger_request
@ -145,6 +146,10 @@ module Gitlab
end end
end end
def job_name_slug(job)
job.name && Gitlab::Utils.slugify(job.name)
end
def ci_node_total_value(job) def ci_node_total_value(job)
parallel = job.options&.dig(:parallel) parallel = job.options&.dig(:parallel)
parallel = parallel.dig(:total) if parallel.is_a?(Hash) parallel = parallel.dig(:total) if parallel.is_a?(Hash)

View File

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

View File

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

View File

@ -6039,6 +6039,9 @@ msgstr ""
msgid "Below you will find all the groups that are public." msgid "Below you will find all the groups that are public."
msgstr "" msgstr ""
msgid "Beta"
msgstr ""
msgid "Bi-weekly code coverage" msgid "Bi-weekly code coverage"
msgstr "" msgstr ""
@ -11162,6 +11165,12 @@ msgstr ""
msgid "Create, update, or delete a merge request." msgid "Create, update, or delete a merge request."
msgstr "" 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." msgid "CreateGroup|You dont have permission to create a subgroup in this group."
msgstr "" msgstr ""
@ -31375,6 +31384,9 @@ msgstr ""
msgid "ProjectSettings|Enable merged results pipelines" msgid "ProjectSettings|Enable merged results pipelines"
msgstr "" msgstr ""
msgid "ProjectSettings|Enable suggested reviewers"
msgstr ""
msgid "ProjectSettings|Encourage" msgid "ProjectSettings|Encourage"
msgstr "" msgstr ""
@ -38801,6 +38813,15 @@ msgstr ""
msgid "SuggestedColors|Titanium yellow" msgid "SuggestedColors|Titanium yellow"
msgstr "" 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." msgid "Suggestion is not applicable as the suggestion was not found."
msgstr "" msgstr ""

View File

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

View File

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

View File

@ -4,6 +4,7 @@
"id", "id",
"username", "username",
"email", "email",
"commit_email",
"name", "name",
"state", "state",
"avatar_url", "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(); await nextTick();
}; };
const dispatchBeforeUnload = () => {
const e = new Event('beforeunload');
jest.spyOn(e, 'preventDefault');
window.dispatchEvent(e);
return e;
};
const pageInfoNew = { const pageInfoNew = {
persisted: false, persisted: false,
uploadsPath: '/project/path/-/wikis/attachments', uploadsPath: '/project/path/-/wikis/attachments',
@ -191,14 +184,6 @@ describe('WikiForm', () => {
expect(wrapper.text()).toContain(text); 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` it.each`
persisted | titleHelpText | titleHelpLink 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'} ${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! '); 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', () => { describe('form submit', () => {
beforeEach(async () => { beforeEach(async () => {
await triggerFormSubmit(); await triggerFormSubmit();
}); });
it('when form submitted, unsets before unload warning', () => {
const e = dispatchBeforeUnload();
expect(e.preventDefault).not.toHaveBeenCalled();
});
it('triggers wiki format tracking event', () => { it('triggers wiki format tracking event', () => {
expect(trackingSpy).toHaveBeenCalledTimes(1); expect(trackingSpy).toHaveBeenCalledTimes(1);
}); });
@ -341,11 +315,6 @@ describe('WikiForm', () => {
await triggerFormSubmit(); 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 () => { it('triggers tracking events on form submit', async () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, { expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
label: WIKI_CONTENT_EDITOR_TRACKING_LABEL, 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 { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale'; import { __ } from '~/locale';
import TagFieldNew from '~/releases/components/tag_field_new.vue'; import TagFieldNew from '~/releases/components/tag_field_new.vue';
import createStore from '~/releases/stores'; import createStore from '~/releases/stores';
import createEditNewModule from '~/releases/stores/modules/edit_new'; import createEditNewModule from '~/releases/stores/modules/edit_new';
const TEST_TAG_NAME = 'test-tag-name'; const TEST_TAG_NAME = 'test-tag-name';
const TEST_TAG_MESSAGE = 'Test tag message';
const TEST_PROJECT_ID = '1234'; const TEST_PROJECT_ID = '1234';
const TEST_CREATE_FROM = 'test-create-from'; const TEST_CREATE_FROM = 'test-create-from';
const NONEXISTENT_TAG_NAME = 'nonexistent-tag'; const NONEXISTENT_TAG_NAME = 'nonexistent-tag';
@ -47,6 +50,8 @@ describe('releases/components/tag_field_new', () => {
store, store,
stubs: { stubs: {
RefSelector: RefSelectorStub, RefSelector: RefSelectorStub,
GlFormGroup,
GlSprintf,
}, },
}); });
}; };
@ -61,9 +66,11 @@ describe('releases/components/tag_field_new', () => {
}); });
store.state.editNew.createFrom = TEST_CREATE_FROM; store.state.editNew.createFrom = TEST_CREATE_FROM;
store.state.editNew.showCreateFrom = true;
store.state.editNew.release = { store.state.editNew.release = {
tagName: TEST_TAG_NAME, tagName: TEST_TAG_NAME,
tagMessage: '',
assets: { assets: {
links: [], links: [],
}, },
@ -86,6 +93,9 @@ describe('releases/components/tag_field_new', () => {
const findCreateNewTagOption = () => wrapper.findComponent(GlDropdownItem); const findCreateNewTagOption = () => wrapper.findComponent(GlDropdownItem);
const findAnnotatedTagMessageFormGroup = () =>
wrapper.find('[data-testid="annotated-tag-message-field"]');
describe('"Tag name" field', () => { describe('"Tag name" field', () => {
describe('rendering and behavior', () => { describe('rendering and behavior', () => {
beforeEach(() => createComponent()); beforeEach(() => createComponent());
@ -124,6 +134,10 @@ describe('releases/components/tag_field_new', () => {
expect(findCreateFromFormGroup().exists()).toBe(false); expect(findCreateFromFormGroup().exists()).toBe(false);
}); });
it('hides the "Tag message" field', () => {
expect(findAnnotatedTagMessageFormGroup().exists()).toBe(false);
});
it('fetches the release notes for the tag', () => { it('fetches the release notes for the tag', () => {
const expectedUrl = `/api/v4/projects/1234/repository/tags/${updatedTagName}`; const expectedUrl = `/api/v4/projects/1234/repository/tags/${updatedTagName}`;
expect(mock.history.get).toContainEqual(expect.objectContaining({ url: expectedUrl })); 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', () => { describe('updateReleasedAt', () => {
it(`commits ${types.UPDATE_RELEASED_AT} with the updated date`, () => { it(`commits ${types.UPDATE_RELEASED_AT} with the updated date`, () => {
const newDate = new 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', () => { it('returns all the data needed for the releaseCreate GraphQL query', () => {
const state = { const state = {
createFrom: 'main', createFrom: 'main',
release: { tagMessage: 'hello' },
}; };
const otherGetters = { const otherGetters = {
@ -352,6 +353,7 @@ describe('Release edit/new getters', () => {
const expectedVariables = { const expectedVariables = {
input: { input: {
name: 'release.name', name: 'release.name',
tagMessage: 'hello',
ref: 'main', ref: 'main',
assets: { assets: {
links: [ links: [

View File

@ -26,6 +26,7 @@ describe('Release edit/new mutations', () => {
expect(state.release).toEqual({ expect(state.release).toEqual({
tagName: 'v1.3', tagName: 'v1.3',
tagMessage: '',
name: '', name: '',
description: '', description: '',
milestones: [], 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}`, () => { describe(`${types.UPDATE_RELEASED_AT}`, () => {
it("updates the release's released at date", () => { it("updates the release's released at date", () => {
state.release = release; 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(:user) { create(:user) }
let_it_be_with_reload(:job) do let_it_be_with_reload(:job) do
create(:ci_build, create(:ci_build,
name: 'rspec:test 1',
pipeline: pipeline, pipeline: pipeline,
user: user, user: user,
yaml_variables: [{ key: 'YAML_VARIABLE', value: 'value' }] 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 let(:predefined_variables) do
[ [
{ key: 'CI_JOB_NAME', { key: 'CI_JOB_NAME',
value: job.name }, value: 'rspec:test 1' },
{ key: 'CI_JOB_NAME_SLUG',
value: 'rspec-test-1' },
{ key: 'CI_JOB_STAGE', { key: 'CI_JOB_STAGE',
value: job.stage_name }, value: job.stage_name },
{ key: 'CI_NODE_TOTAL', { key: 'CI_NODE_TOTAL',
value: '1' }, value: '1' },
{ key: 'CI_BUILD_NAME', { key: 'CI_BUILD_NAME',
value: job.name }, value: 'rspec:test 1' },
{ key: 'CI_BUILD_STAGE', { key: 'CI_BUILD_STAGE',
value: job.stage_name }, value: job.stage_name },
{ key: 'CI', { key: 'CI',

View File

@ -207,6 +207,16 @@ RSpec.describe Sidebars::Groups::Menus::PackagesRegistriesMenu do
it_behaves_like 'the menu entry is available' it_behaves_like 'the menu entry is available'
end 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
end end

View File

@ -166,6 +166,15 @@ RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
is_expected.not_to be_nil is_expected.not_to be_nil
end end
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 end
end end

View File

@ -86,9 +86,9 @@ RSpec.describe Ci::Bridge do
describe '#scoped_variables' do describe '#scoped_variables' do
it 'returns a hash representing variables' do it 'returns a hash representing variables' do
variables = %w[ variables = %w[
CI_JOB_NAME CI_JOB_STAGE CI_COMMIT_SHA CI_COMMIT_SHORT_SHA CI_JOB_NAME CI_JOB_NAME_SLUG CI_JOB_STAGE CI_COMMIT_SHA
CI_COMMIT_BEFORE_SHA CI_COMMIT_REF_NAME CI_COMMIT_REF_SLUG CI_COMMIT_SHORT_SHA CI_COMMIT_BEFORE_SHA CI_COMMIT_REF_NAME
CI_PROJECT_ID CI_PROJECT_NAME CI_PROJECT_PATH 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_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PROJECT_ROOT_NAMESPACE
CI_PIPELINE_IID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE CI_PIPELINE_IID CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE
CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED 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_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_JWT_V2', value: 'ci.job.jwtv2', public: false, masked: true },
{ key: 'CI_JOB_NAME', value: 'test', public: true, masked: false }, { 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_JOB_STAGE', value: 'test', public: true, masked: false },
{ key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false }, { key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false },
{ key: 'CI_BUILD_NAME', value: 'test', 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 end
before do 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) stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end 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 context 'when the total number of records to be inserted is greater than the batch size' do
let(:per_batch_size) { 2 } 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)) expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values))
end 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 end
context 'when the total number of records to be inserted is less than the batch size' do context 'when the total number of records to be inserted is less than the batch size' do
let(:per_batch_size) { 5 } let(:per_batch_size) { 5 }
it 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch' do it_behaves_like 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch'
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
end end
end end
@ -142,7 +155,8 @@ RSpec.describe ProjectAuthorization do
let(:user_ids) { [user_1.id, user_2.id, user_3.id] } let(:user_ids) { [user_1.id, user_2.id, user_3.id] }
before do 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) stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end end
@ -153,6 +167,20 @@ RSpec.describe ProjectAuthorization do
create(:project_authorization, user: user_4, project: project) create(:project_authorization, user: user_4, project: project)
end 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 context 'when the total number of records to be removed is greater than the batch size' do
let(:per_batch_size) { 2 } 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) expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids)
end 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 end
context 'when the total number of records to be removed is less than the batch size' do context 'when the total number of records to be removed is less than the batch size' do
let(:per_batch_size) { 5 } 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 it_behaves_like 'removes the project authorizations of the specified users in the current project, without a delay between each batch'
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 end
end end
@ -196,7 +223,8 @@ RSpec.describe ProjectAuthorization do
let(:project_ids) { [project_1.id, project_2.id, project_3.id] } let(:project_ids) { [project_1.id, project_2.id, project_3.id] }
before do 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) stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end end
@ -207,26 +235,8 @@ RSpec.describe ProjectAuthorization do
create(:project_authorization, user: user, project: project_4) create(:project_authorization, user: user, project: project_4)
end end
context 'when the total number of records to be removed is greater than the batch size' do shared_examples_for 'removes the project authorizations of the specified projects from the current user, without a delay between each batch' do
let(:per_batch_size) { 2 } specify do
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
expect(described_class).not_to receive(:sleep) expect(described_class).not_to receive(:sleep)
described_class.delete_all_in_batches_for_user( 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) expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids)
end end
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
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.not_to allow_value(nil).for(:target_platforms) }
it { is_expected.to allow_value([]).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 it 'allows any combination of the allowed target platforms' do
valid_target_platform_combinations.each do |target_platforms| valid_target_platform_combinations.each do |target_platforms|
expect(subject).to allow_value(target_platforms).for(:target_platforms) expect(subject).to allow_value(target_platforms).for(:target_platforms)

View File

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

View File

@ -4,15 +4,29 @@ require 'spec_helper'
RSpec.describe GoogleCloud::EnableCloudsqlService do RSpec.describe GoogleCloud::EnableCloudsqlService do
let_it_be(:project) { create(:project) } 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 context 'when a project does not have any GCP_PROJECT_IDs configured' do
it 'returns error' do it 'creates GCP_PROJECT_ID project var' do
message = 'No GCP projects found. Configure a service account or GCP_PROJECT_ID CI variable.' 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[:status]).to eq(:success)
expect(result[:message]).to eq(message) 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
end end
@ -30,6 +44,9 @@ RSpec.describe GoogleCloud::EnableCloudsqlService do
it 'enables cloudsql, compute and service networking Google APIs', :aggregate_failures do it 'enables cloudsql, compute and service networking Google APIs', :aggregate_failures do
expect_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance| 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_cloud_sql_admin).with('prj-prod')
expect(instance).to receive(:enable_compute).with('prj-prod') expect(instance).to receive(:enable_compute).with('prj-prod')
expect(instance).to receive(:enable_service_networking).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 context 'when Google APIs raise an error' do
it 'returns error result' do it 'returns error result' do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |instance| 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_cloud_sql_admin).with('prj-prod')
allow(instance).to receive(:enable_compute).with('prj-prod') allow(instance).to receive(:enable_compute).with('prj-prod')
allow(instance).to receive(:enable_service_networking).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) attach_file('content_editor_image', Rails.root.join('spec', 'fixtures', fixture_name), make_visible: true)
end 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 describe 'formatting bubble menu' do
it 'shows a formatting bubble menu for a regular paragraph and headings' do it 'shows a formatting bubble menu for a regular paragraph and headings' do
switch_to_content_editor switch_to_content_editor

View File

@ -147,6 +147,18 @@ RSpec.shared_examples 'User creates wiki page' do
end end
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 it 'creates a wiki page with Org markup', :aggregate_failures, :js do
org_content = <<~ORG org_content = <<~ORG
* Heading * Heading

View File

@ -78,6 +78,18 @@ RSpec.shared_examples 'User updates wiki page' do
expect(page).to have_content('My awesome wiki!') expect(page).to have_content('My awesome wiki!')
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 'updates the commit message as the title is changed', :js do it 'updates the commit message as the title is changed', :js do
fill_in(:wiki_title, with: '& < > \ \ { } &') 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_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true) 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(:list_projects).and_return(mock_gcp_projects) if mock_gcp_projects
allow(client).to receive(:create_cloudsql_instance)
end end
allow_next_instance_of(BranchesFinder) do |finder| allow_next_instance_of(BranchesFinder) do |finder|

View File

@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'admin/broadcast_messages/index' do RSpec.describe 'admin/broadcast_messages/index' do
describe 'Target roles select and table column' do let(:role_targeted_broadcast_messages) { true }
let(:feature_flag_state) { 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]) } let_it_be(:message) { create(:broadcast_message, broadcast_type: 'banner', target_access_levels: [Gitlab::Access::GUEST, Gitlab::Access::DEVELOPER]) }
@ -12,11 +12,13 @@ RSpec.describe 'admin/broadcast_messages/index' do
assign(:broadcast_messages, BroadcastMessage.page(1)) assign(:broadcast_messages, BroadcastMessage.page(1))
assign(:broadcast_message, BroadcastMessage.new) assign(:broadcast_message, BroadcastMessage.new)
stub_feature_flags(role_targeted_broadcast_messages: feature_flag_state) stub_feature_flags(role_targeted_broadcast_messages: role_targeted_broadcast_messages)
stub_feature_flags(vue_broadcast_messages: vue_broadcast_messages)
render render
end end
describe 'Target roles select and table column' do
it 'rendered' do it 'rendered' do
expect(rendered).to have_content('Target roles') expect(rendered).to have_content('Target roles')
expect(rendered).to have_content('Owner') expect(rendered).to have_content('Owner')
@ -24,7 +26,7 @@ RSpec.describe 'admin/broadcast_messages/index' do
end end
context 'when feature flag is off' do context 'when feature flag is off' do
let(:feature_flag_state) { false } let(:role_targeted_broadcast_messages) { false }
it 'is not rendered' do it 'is not rendered' do
expect(rendered).not_to have_content('Target roles') expect(rendered).not_to have_content('Target roles')
@ -33,4 +35,18 @@ RSpec.describe 'admin/broadcast_messages/index' do
end end
end 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 end

View File

@ -1 +1 @@
golang 1.17.9 golang 1.18.6