Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-17 15:11:57 +00:00
parent 458b945df3
commit 5ff5438a06
68 changed files with 1173 additions and 240 deletions

View File

@ -1118,7 +1118,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/ee/lib/audit/group_push_rules_changes_auditor.rb @gitlab-org/manage/compliance
/ee/lib/ee/api/entities/audit_event.rb @gitlab-org/manage/compliance
/ee/lib/ee/audit/ @gitlab-org/manage/compliance
/ee/lib/gitlab/audit/auditor.rb @gitlab-org/manage/compliance
/ee/lib/ee/gitlab/audit/ @gitlab-org/manage/compliance
/ee/spec/controllers/admin/audit_log_reports_controller_spec.rb @gitlab-org/manage/compliance
/ee/spec/controllers/admin/audit_logs_controller_spec.rb @gitlab-org/manage/compliance
/ee/spec/controllers/groups/audit_events_controller_spec.rb @gitlab-org/manage/compliance
@ -1163,13 +1163,10 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab
/ee/spec/support/shared_examples/features/audit_events_filter_shared_examples.rb @gitlab-org/manage/compliance
/ee/spec/support/shared_examples/services/audit_event_logging_shared_examples.rb @gitlab-org/manage/compliance
/ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb @gitlab-org/manage/compliance
/lib/gitlab/audit/auditor.rb @gitlab-org/manage/compliance
/lib/gitlab/audit_json_logger.rb @gitlab-org/manage/compliance
/qa/qa/ee/page/admin/monitoring/ @gitlab-org/manage/compliance
/qa/qa/specs/features/ee/browser_ui/1_manage/group/group_audit_logs_1_spec.rb @gitlab-org/manage/compliance
/qa/qa/specs/features/ee/browser_ui/1_manage/group/group_audit_logs_2_spec.rb @gitlab-org/manage/compliance
/qa/qa/specs/features/ee/browser_ui/1_manage/instance/ @gitlab-org/manage/compliance
/qa/qa/specs/features/ee/browser_ui/1_manage/project/project_audit_logs_spec.rb @gitlab-org/manage/compliance
/spec/factories/audit_events.rb @gitlab-org/manage/compliance
/spec/lib/gitlab/audit/auditor_spec.rb @gitlab-org/manage/compliance
/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb @gitlab-org/manage/compliance
/spec/models/audit_event_spec.rb @gitlab-org/manage/compliance
/spec/services/audit_event_service_spec.rb @gitlab-org/manage/compliance

View File

@ -0,0 +1,104 @@
<script>
import createFlash from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
GRAPHQL_GROUP_TYPE,
UPDATE_MUTATION_ACTION,
genericMutationErrorText,
variableFetchErrorText,
} from '../constants';
import addGroupVariable from '../graphql/mutations/group_add_variable.mutation.graphql';
import deleteGroupVariable from '../graphql/mutations/group_delete_variable.mutation.graphql';
import updateGroupVariable from '../graphql/mutations/group_update_variable.mutation.graphql';
import ciVariableSettings from './ci_variable_settings.vue';
export default {
components: {
ciVariableSettings,
},
mixins: [glFeatureFlagsMixin()],
inject: ['endpoint', 'groupPath', 'groupId'],
data() {
return {
groupVariables: [],
};
},
apollo: {
groupVariables: {
query: getGroupVariables,
variables() {
return {
fullPath: this.groupPath,
};
},
update(data) {
return data?.group?.ciVariables?.nodes || [];
},
error() {
createFlash({ message: variableFetchErrorText });
},
},
},
computed: {
areScopedVariablesAvailable() {
return this.glFeatures.groupScopedCiVariables;
},
isLoading() {
return this.$apollo.queries.groupVariables.loading;
},
},
methods: {
addVariable(variable) {
this.variableMutation(ADD_MUTATION_ACTION, variable);
},
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
async variableMutation(mutationAction, variable) {
try {
const currentMutation = this.$options.mutationData[mutationAction];
const { data } = await this.$apollo.mutate({
mutation: currentMutation.action,
variables: {
endpoint: this.endpoint,
fullPath: this.groupPath,
groupId: convertToGraphQLId(GRAPHQL_GROUP_TYPE, this.groupId),
variable,
},
});
const { errors } = data[currentMutation.name];
if (errors.length > 0) {
createFlash({ message: errors[0] });
}
} catch {
createFlash({ message: genericMutationErrorText });
}
},
},
mutationData: {
[ADD_MUTATION_ACTION]: { action: addGroupVariable, name: 'addGroupVariable' },
[UPDATE_MUTATION_ACTION]: { action: updateGroupVariable, name: 'updateGroupVariable' },
[DELETE_MUTATION_ACTION]: { action: deleteGroupVariable, name: 'deleteGroupVariable' },
},
};
</script>
<template>
<ci-variable-settings
:are-scoped-variables-available="areScopedVariablesAvailable"
:is-loading="isLoading"
:variables="groupVariables"
@add-variable="addVariable"
@delete-variable="deleteVariable"
@update-variable="updateVariable"
/>
</template>

View File

@ -0,0 +1,30 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation addGroupVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
$groupId: ID!
) {
addGroupVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
groupId: $groupId
) @client {
group {
id
ciVariables {
nodes {
...BaseCiVariable
... on CiGroupVariable {
environmentScope
masked
protected
}
}
}
}
errors
}
}

View File

@ -0,0 +1,30 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation deleteGroupVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
$groupId: ID!
) {
deleteGroupVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
groupId: $groupId
) @client {
group {
id
ciVariables {
nodes {
...BaseCiVariable
... on CiGroupVariable {
environmentScope
masked
protected
}
}
}
}
errors
}
}

View File

@ -0,0 +1,30 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation updateGroupVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
$groupId: ID!
) {
updateGroupVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
groupId: $groupId
) @client {
group {
id
ciVariables {
nodes {
...BaseCiVariable
... on CiGroupVariable {
environmentScope
masked
protected
}
}
}
}
errors
}
}

View File

@ -0,0 +1,17 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
query getGroupVariables($fullPath: ID!) {
group(fullPath: $fullPath) {
id
ciVariables {
nodes {
...BaseCiVariable
... on CiGroupVariable {
environmentScope
masked
protected
}
}
}
}
}

View File

@ -4,8 +4,9 @@ import {
convertObjectPropsToSnakeCase,
} from '../../lib/utils/common_utils';
import { getIdFromGraphQLId } from '../../graphql_shared/utils';
import { instanceString } from '../constants';
import { GRAPHQL_GROUP_TYPE, groupString, instanceString } from '../constants';
import getAdminVariables from './queries/variables.query.graphql';
import getGroupVariables from './queries/group_variables.query.graphql';
const prepareVariableForApi = ({ variable, destroy = false }) => {
return {
@ -27,6 +28,20 @@ const mapVariableTypes = (variables = [], kind) => {
});
};
const prepareGroupGraphQLResponse = ({ data, groupId, errors = [] }) => {
return {
errors,
group: {
__typename: GRAPHQL_GROUP_TYPE,
id: groupId,
ciVariables: {
__typename: 'CiVariableConnection',
nodes: mapVariableTypes(data.variables, groupString),
},
},
};
};
const prepareAdminGraphQLResponse = ({ data, errors = [] }) => {
return {
errors,
@ -37,6 +52,28 @@ const prepareAdminGraphQLResponse = ({ data, errors = [] }) => {
};
};
const callGroupEndpoint = async ({
endpoint,
fullPath,
variable,
groupId,
cache,
destroy = false,
}) => {
try {
const { data } = await axios.patch(endpoint, {
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
return prepareGroupGraphQLResponse({ data, groupId });
} catch (e) {
return prepareGroupGraphQLResponse({
data: cache.readQuery({ query: getGroupVariables, variables: { fullPath } }),
groupId,
errors: [...e.response.data],
});
}
};
const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false }) => {
try {
const { data } = await axios.patch(endpoint, {
@ -54,6 +91,15 @@ const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false })
export const resolvers = {
Mutation: {
addGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache });
},
updateGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache });
},
deleteGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache, destroy: true });
},
addAdminVariable: async (_, { endpoint, variable }, { cache }) => {
return callAdminEndpoint({ endpoint, variable, cache });
},

View File

@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import CiAdminVariables from './components/ci_admin_variables.vue';
import CiGroupVariables from './components/ci_group_variables.vue';
import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
import { resolvers } from './graphql/resolvers';
import createStore from './store';
@ -32,7 +33,11 @@ const mountCiVariableListApp = (containerEl) => {
const parsedIsGroup = parseBoolean(isGroup);
const isProtectedByDefault = parseBoolean(protectedByDefault);
const component = CiAdminVariables;
let component = CiAdminVariables;
if (parsedIsGroup) {
component = CiGroupVariables;
}
Vue.use(VueApollo);

View File

@ -23,6 +23,9 @@ import {
SEARCH_SHORTCUTS_MIN_CHARACTERS,
SCOPE_TOKEN_MAX_LENGTH,
INPUT_FIELD_PADDING,
IS_SEARCHING,
IS_FOCUSED,
IS_NOT_FOCUSED,
} from '../constants';
import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue';
import HeaderSearchDefaultItems from './header_search_default_items.vue';
@ -65,6 +68,7 @@ export default {
data() {
return {
showDropdown: false,
isFocused: false,
currentFocusIndex: SEARCH_BOX_INDEX,
};
},
@ -92,20 +96,18 @@ export default {
if (!this.showDropdown || !this.isLoggedIn) {
return false;
}
return this.searchOptions?.length > 0;
},
showDefaultItems() {
return !this.searchText;
},
showScopes() {
searchTermOverMin() {
return this.searchText?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
},
defaultIndex() {
if (this.showDefaultItems) {
return SEARCH_BOX_INDEX;
}
return FIRST_DROPDOWN_INDEX;
},
@ -132,12 +134,15 @@ export default {
count: this.searchOptions.length,
});
},
searchBarStateIndicator() {
const hasIcon =
this.searchContext?.project || this.searchContext?.group ? 'has-icon' : 'has-no-icon';
const isSearching = this.showScopes ? 'is-searching' : 'is-not-searching';
const isActive = this.showSearchDropdown ? 'is-active' : 'is-not-active';
return `${isActive} ${isSearching} ${hasIcon}`;
searchBarClasses() {
return {
[IS_SEARCHING]: this.searchTermOverMin,
[IS_FOCUSED]: this.isFocused,
[IS_NOT_FOCUSED]: !this.isFocused,
};
},
showScopeHelp() {
return this.searchTermOverMin && this.isFocused;
},
searchBarItem() {
return this.searchOptions?.[0];
@ -158,11 +163,22 @@ export default {
...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
openDropdown() {
this.showDropdown = true;
this.$emit('toggleDropdown', this.showDropdown);
this.isFocused = true;
this.$emit('expandSearchBar', true);
},
closeDropdown() {
this.showDropdown = false;
this.$emit('toggleDropdown', this.showDropdown);
},
collapseAndCloseSearchBar() {
// we need a delay on this method
// for the search bar not to remove
// the clear button from dom
// and register clicks on dropdown items
setTimeout(() => {
this.showDropdown = false;
this.isFocused = false;
this.$emit('collapseSearchBar');
}, 200);
},
submitSearch() {
if (this.search?.length <= SEARCH_SHORTCUTS_MIN_CHARACTERS && this.currentFocusIndex < 0) {
@ -171,6 +187,7 @@ export default {
return visitUrl(this.currentFocusedOption?.url || this.searchQuery);
},
getAutocompleteOptions: debounce(function debouncedSearch(searchTerm) {
this.openDropdown();
if (!searchTerm) {
this.clearAutocomplete();
} else {
@ -201,7 +218,7 @@ export default {
role="search"
:aria-label="$options.i18n.searchGitlab"
class="header-search gl-relative gl-rounded-base gl-w-full"
:class="searchBarStateIndicator"
:class="searchBarClasses"
data-testid="header-search-form"
>
<gl-search-box-by-type
@ -217,12 +234,13 @@ export default {
:aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
@focus="openDropdown"
@click="openDropdown"
@blur="collapseAndCloseSearchBar"
@input="getAutocompleteOptions"
@keydown.enter.stop.prevent="submitSearch"
@keydown.esc.stop.prevent="closeDropdown"
/>
<gl-token
v-if="showScopes"
v-if="showScopeHelp"
v-gl-resize-observer-directive="observeTokenWidth"
class="in-search-scope-help"
:view-only="true"
@ -242,6 +260,7 @@ export default {
}}
</gl-token>
<kbd
v-show="!isFocused"
v-gl-tooltip.bottom.hover.html
class="gl-absolute gl-right-3 gl-top-0 gl-z-index-1 keyboard-shortcut-helper"
:title="$options.i18n.kbdHelp"
@ -278,7 +297,7 @@ export default {
/>
<template v-else>
<header-search-scoped-items
v-if="showScopes"
v-if="searchTermOverMin"
:current-focused-option="currentFocusedOption"
/>
<header-search-autocomplete-items :current-focused-option="currentFocusedOption" />

View File

@ -51,3 +51,7 @@ export const SCOPE_TOKEN_MAX_LENGTH = 36;
export const INPUT_FIELD_PADDING = 52;
export const HEADER_INIT_EVENTS = ['input', 'focus'];
export const IS_SEARCHING = 'is-searching';
export const IS_FOCUSED = 'is-focused';
export const IS_NOT_FOCUSED = 'is-not-focused';

View File

@ -26,12 +26,11 @@ export const initHeaderSearchApp = (search = '') => {
render(createElement) {
return createElement(HeaderSearchApp, {
on: {
toggleDropdown: (isVisible = false) => {
if (isVisible) {
navBarEl?.classList.add('header-search-is-active');
} else {
navBarEl?.classList.remove('header-search-is-active');
}
expandSearchBar: () => {
navBarEl?.classList.add('header-search-is-active');
},
collapseSearchBar: () => {
navBarEl?.classList.remove('header-search-is-active');
},
},
});

View File

@ -68,7 +68,7 @@ export default {
}),
tableCell({
key: 'created_at',
label: __('Date'),
label: __('Start date'),
}),
tableCell({
key: 'status',

View File

@ -264,9 +264,15 @@ export default {
data-testid="work-item-type"
/>
<gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
<gl-badge v-if="workItem.confidential" variant="warning" icon="eye-slash" class="gl-mr-3">{{
__('Confidential')
}}</gl-badge>
<gl-badge
v-if="workItem.confidential"
v-gl-tooltip.bottom
:title="$options.i18n.confidentialTooltip"
variant="warning"
icon="eye-slash"
class="gl-mr-3 gl-cursor-help"
>{{ __('Confidential') }}</gl-badge
>
<work-item-actions
v-if="canUpdate || canDelete"
:work-item-id="workItem.id"

View File

@ -26,6 +26,9 @@ export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const i18n = {
fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
confidentialTooltip: s__(
'WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this task.',
),
};
export const WIDGET_ICONS = {

View File

@ -82,19 +82,17 @@ input[type='checkbox']:hover {
min-width: $search-input-field-x-min-width;
}
&.is-active {
&.is-searching {
.in-search-scope-help {
position: absolute;
top: $gl-spacing-scale-2;
right: 2.125rem;
z-index: 2;
}
&.is-searching {
.in-search-scope-help {
position: absolute;
top: $gl-spacing-scale-2;
right: 2.125rem;
z-index: 2;
}
}
&.is-not-searching {
.in-search-scope-help {
&.is-not-focused {
.gl-search-box-by-type-clear {
display: none;
}
}
@ -104,19 +102,6 @@ input[type='checkbox']:hover {
box-shadow: none;
border-color: transparent;
}
&.is-active {
.keyboard-shortcut-helper {
display: none;
}
}
&.is-not-active {
.btn.gl-clear-icon-button,
.in-search-scope-help {
display: none;
}
}
}
.header-search-dropdown-menu {

View File

@ -1892,7 +1892,7 @@ body.gl-dark .header-search input::placeholder {
body.gl-dark .header-search input:active::placeholder {
color: #868686;
}
body.gl-dark .header-search.is-not-active .keyboard-shortcut-helper {
body.gl-dark .header-search .keyboard-shortcut-helper {
color: #fafafa;
background-color: rgba(250, 250, 250, 0.2);
}

View File

@ -176,11 +176,9 @@
}
}
&.is-not-active {
.keyboard-shortcut-helper {
color: $search-and-nav-links;
background-color: rgba($search-and-nav-links, 0.2);
}
.keyboard-shortcut-helper {
color: $search-and-nav-links;
background-color: rgba($search-and-nav-links, 0.2);
}
}

View File

@ -10,6 +10,9 @@ module Groups
before_action :define_variables, only: [:show]
before_action :push_licensed_features, only: [:show]
before_action :assign_variables_to_gon, only: [:show]
before_action do
push_frontend_feature_flag(:ci_variable_settings_graphql, @group)
end
feature_category :continuous_integration
urgency :low

6
app/models/ml.rb Normal file
View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
module Ml
def self.table_name_prefix
'ml_'
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Ml
class Candidate < ApplicationRecord
validates :iid, :experiment, presence: true
belongs_to :experiment, class_name: 'Ml::Experiment'
belongs_to :user
has_many :metrics, class_name: 'Ml::CandidateMetric'
has_many :params, class_name: 'Ml::CandidateParam'
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Ml
class CandidateMetric < ApplicationRecord
validates :candidate, presence: true
validates :name, length: { maximum: 250 }, presence: true
belongs_to :candidate, class_name: 'Ml::Candidate'
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Ml
class CandidateParam < ApplicationRecord
validates :candidate, presence: true
validates :name, :value, length: { maximum: 250 }, presence: true
belongs_to :candidate, class_name: 'Ml::Candidate'
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Ml
class Experiment < ApplicationRecord
validates :name, :iid, :project, presence: true
validates :iid, :name, uniqueness: { scope: :project, message: "should be unique in the project" }
belongs_to :project
belongs_to :user
has_many :candidates, class_name: 'Ml::Candidate'
end
end

View File

@ -0,0 +1,10 @@
---
table_name: ml_candidate_metrics
classes:
- Ml::CandidateMetric
feature_categories:
- mlops
- incubation
description: Metrics recorded for a Machine Learning model candidate
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95168
milestone: '15.4'

View File

@ -0,0 +1,10 @@
---
table_name: ml_candidate_params
classes:
- Ml::CandidateParams
feature_categories:
- mlops
- incubation
description: Configuration parameters recorded for a Machine Learning model candidate
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95168
milestone: '15.4'

10
db/docs/ml_candidates.yml Normal file
View File

@ -0,0 +1,10 @@
---
table_name: ml_candidates
classes:
- Ml::Candidate
feature_categories:
- mlops
- incubation
description: A Model Candidate is a record of the results on training a model on some configuration
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95168
milestone: '15.4'

View File

@ -0,0 +1,10 @@
---
table_name: ml_experiments
classes:
- Ml::Experiment
feature_categories:
- mlops
- incubation
description: A Machine Learning Experiments groups many Model Candidates
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95168
milestone: '15.4'

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class CreateMlExperiments < Gitlab::Database::Migration[2.0]
enable_lock_retries!
def change
create_table :ml_experiments do |t|
t.timestamps_with_timezone null: false
t.bigint :iid, null: false
t.bigint :project_id, null: false
t.references :user, foreign_key: true, index: true, on_delete: :nullify
t.text :name, limit: 255, null: false
t.index [:project_id, :iid], unique: true
t.index [:project_id, :name], unique: true
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class CreateMlCandidates < Gitlab::Database::Migration[2.0]
enable_lock_retries!
def change
create_table :ml_candidates do |t|
t.timestamps_with_timezone null: false
t.uuid :iid, null: false
t.bigint :experiment_id, null: false
t.references :user, foreign_key: true, index: true, on_delete: :nullify
t.index [:experiment_id, :iid], unique: true
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class CreateMlCandidateParams < Gitlab::Database::Migration[2.0]
def change
create_table :ml_candidate_params do |t|
t.timestamps_with_timezone null: false
t.references :candidate,
foreign_key: { to_table: :ml_candidates },
index: true
t.text :name, limit: 250, null: false
t.text :value, limit: 250, null: false
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class CreateMlCandidateMetrics < Gitlab::Database::Migration[2.0]
def change
create_table :ml_candidate_metrics do |t|
t.timestamps_with_timezone null: false
t.references :candidate,
foreign_key: { to_table: :ml_candidates },
index: true
t.float :value
t.integer :step
t.binary :is_nan
t.text :name, limit: 250, null: false
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddMlCandidatesReferenceToExperiment < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :ml_candidates, :ml_experiments, column: :experiment_id
end
def down
with_lock_retries do
remove_foreign_key :ml_candidates, column: :experiment_id
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddMlExperimentsReferenceToProject < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
add_concurrent_foreign_key :ml_experiments, :projects, column: :project_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :ml_experiments, column: :project_id
end
end
end

View File

@ -0,0 +1 @@
211eda22a78d14aaaf86345d3e33b852ba22a7dc9e41d9d683d58f162a7bdcc7

View File

@ -0,0 +1 @@
f871847fbd494e31f13cf2fb87a1b8e9fc47c44e7f0ec9cf37f2084d19b9bf5f

View File

@ -0,0 +1 @@
0c856ce8170e4b864578f1bcb89d8930d8c1952e92356965a98e057521456968

View File

@ -0,0 +1 @@
17bcb2fddd6331cbcec505e8094d1a400b7c3fd8b18897697aa9868689147cd7

View File

@ -0,0 +1 @@
4ea4bc7e6f88561553b19c7bf4992561772506cf532cf569241a536f69e19b7f

View File

@ -0,0 +1 @@
6a6eed069e051786a925b40469e7b53a563f99f0c6bfb810058511d3de8b0923

View File

@ -17596,6 +17596,85 @@ CREATE SEQUENCE milestones_id_seq
ALTER SEQUENCE milestones_id_seq OWNED BY milestones.id;
CREATE TABLE ml_candidate_metrics (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
candidate_id bigint,
value double precision,
step integer,
is_nan bytea,
name text NOT NULL,
CONSTRAINT check_3bb4a3fbd9 CHECK ((char_length(name) <= 250))
);
CREATE SEQUENCE ml_candidate_metrics_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE ml_candidate_metrics_id_seq OWNED BY ml_candidate_metrics.id;
CREATE TABLE ml_candidate_params (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
candidate_id bigint,
name text NOT NULL,
value text NOT NULL,
CONSTRAINT check_093034d049 CHECK ((char_length(name) <= 250)),
CONSTRAINT check_28a3c29e43 CHECK ((char_length(value) <= 250))
);
CREATE SEQUENCE ml_candidate_params_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE ml_candidate_params_id_seq OWNED BY ml_candidate_params.id;
CREATE TABLE ml_candidates (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
iid uuid NOT NULL,
experiment_id bigint NOT NULL,
user_id bigint
);
CREATE SEQUENCE ml_candidates_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE ml_candidates_id_seq OWNED BY ml_candidates.id;
CREATE TABLE ml_experiments (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
iid bigint NOT NULL,
project_id bigint NOT NULL,
user_id bigint,
name text NOT NULL,
CONSTRAINT check_ee07a0be2c CHECK ((char_length(name) <= 255))
);
CREATE SEQUENCE ml_experiments_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE ml_experiments_id_seq OWNED BY ml_experiments.id;
CREATE TABLE namespace_admin_notes (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -23468,6 +23547,14 @@ ALTER TABLE ONLY metrics_users_starred_dashboards ALTER COLUMN id SET DEFAULT ne
ALTER TABLE ONLY milestones ALTER COLUMN id SET DEFAULT nextval('milestones_id_seq'::regclass);
ALTER TABLE ONLY ml_candidate_metrics ALTER COLUMN id SET DEFAULT nextval('ml_candidate_metrics_id_seq'::regclass);
ALTER TABLE ONLY ml_candidate_params ALTER COLUMN id SET DEFAULT nextval('ml_candidate_params_id_seq'::regclass);
ALTER TABLE ONLY ml_candidates ALTER COLUMN id SET DEFAULT nextval('ml_candidates_id_seq'::regclass);
ALTER TABLE ONLY ml_experiments ALTER COLUMN id SET DEFAULT nextval('ml_experiments_id_seq'::regclass);
ALTER TABLE ONLY namespace_admin_notes ALTER COLUMN id SET DEFAULT nextval('namespace_admin_notes_id_seq'::regclass);
ALTER TABLE ONLY namespace_bans ALTER COLUMN id SET DEFAULT nextval('namespace_bans_id_seq'::regclass);
@ -25479,6 +25566,18 @@ ALTER TABLE ONLY milestone_releases
ALTER TABLE ONLY milestones
ADD CONSTRAINT milestones_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ml_candidate_metrics
ADD CONSTRAINT ml_candidate_metrics_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ml_candidate_params
ADD CONSTRAINT ml_candidate_params_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ml_candidates
ADD CONSTRAINT ml_candidates_pkey PRIMARY KEY (id);
ALTER TABLE ONLY ml_experiments
ADD CONSTRAINT ml_experiments_pkey PRIMARY KEY (id);
ALTER TABLE ONLY namespace_admin_notes
ADD CONSTRAINT namespace_admin_notes_pkey PRIMARY KEY (id);
@ -29037,6 +29136,20 @@ CREATE INDEX index_milestones_on_title_trigram ON milestones USING gin (title gi
CREATE INDEX index_mirror_data_non_scheduled_or_started ON project_mirror_data USING btree (next_execution_timestamp, retry_count) WHERE ((status)::text <> ALL ('{scheduled,started}'::text[]));
CREATE INDEX index_ml_candidate_metrics_on_candidate_id ON ml_candidate_metrics USING btree (candidate_id);
CREATE INDEX index_ml_candidate_params_on_candidate_id ON ml_candidate_params USING btree (candidate_id);
CREATE UNIQUE INDEX index_ml_candidates_on_experiment_id_and_iid ON ml_candidates USING btree (experiment_id, iid);
CREATE INDEX index_ml_candidates_on_user_id ON ml_candidates USING btree (user_id);
CREATE UNIQUE INDEX index_ml_experiments_on_project_id_and_iid ON ml_experiments USING btree (project_id, iid);
CREATE UNIQUE INDEX index_ml_experiments_on_project_id_and_name ON ml_experiments USING btree (project_id, name);
CREATE INDEX index_ml_experiments_on_user_id ON ml_experiments USING btree (user_id);
CREATE UNIQUE INDEX index_mr_blocks_on_blocking_and_blocked_mr_ids ON merge_request_blocks USING btree (blocking_merge_request_id, blocked_merge_request_id);
CREATE INDEX index_mr_cleanup_schedules_timestamps_status ON merge_request_cleanup_schedules USING btree (scheduled_at) WHERE ((completed_at IS NULL) AND (status = 0));
@ -32180,6 +32293,9 @@ ALTER TABLE ONLY merge_request_metrics
ALTER TABLE ONLY vulnerability_feedback
ADD CONSTRAINT fk_563ff1912e FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE SET NULL;
ALTER TABLE ONLY ml_candidates
ADD CONSTRAINT fk_56d6ed4d3d FOREIGN KEY (experiment_id) REFERENCES ml_experiments(id) ON DELETE CASCADE;
ALTER TABLE ONLY deploy_keys_projects
ADD CONSTRAINT fk_58a901ca7e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
@ -32477,6 +32593,9 @@ ALTER TABLE ONLY member_tasks
ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_ad525e1f87 FOREIGN KEY (merge_user_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY ml_experiments
ADD CONSTRAINT fk_ad89c59858 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY merge_request_metrics
ADD CONSTRAINT fk_ae440388cc FOREIGN KEY (latest_closed_by_id) REFERENCES users(id) ON DELETE SET NULL;
@ -32987,6 +33106,9 @@ ALTER TABLE ONLY vulnerability_user_mentions
ALTER TABLE ONLY packages_debian_file_metadata
ADD CONSTRAINT fk_rails_1ae85be112 FOREIGN KEY (package_file_id) REFERENCES packages_package_files(id) ON DELETE CASCADE;
ALTER TABLE ONLY ml_candidates
ADD CONSTRAINT fk_rails_1b37441fe5 FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE ONLY issuable_slas
ADD CONSTRAINT fk_rails_1b8768cd63 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
@ -33014,6 +33136,9 @@ ALTER TABLE ONLY geo_repository_created_events
ALTER TABLE ONLY external_status_checks
ADD CONSTRAINT fk_rails_1f5a8aa809 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY ml_experiments
ADD CONSTRAINT fk_rails_1fbc5e001f FOREIGN KEY (user_id) REFERENCES users(id);
ALTER TABLE ONLY dora_daily_metrics
ADD CONSTRAINT fk_rails_1fd07aff6f FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE;
@ -34145,6 +34270,9 @@ ALTER TABLE ONLY alert_management_alert_assignees
ALTER TABLE ONLY geo_hashed_storage_attachments_events
ADD CONSTRAINT fk_rails_d496b088e9 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY ml_candidate_params
ADD CONSTRAINT fk_rails_d4a51d1185 FOREIGN KEY (candidate_id) REFERENCES ml_candidates(id);
ALTER TABLE ONLY merge_request_reviewers
ADD CONSTRAINT fk_rails_d9fec24b9d FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
@ -34292,6 +34420,9 @@ ALTER TABLE ONLY project_relation_exports
ALTER TABLE ONLY label_priorities
ADD CONSTRAINT fk_rails_ef916d14fa FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY ml_candidate_metrics
ADD CONSTRAINT fk_rails_efb613a25a FOREIGN KEY (candidate_id) REFERENCES ml_candidates(id);
ALTER TABLE ONLY fork_network_members
ADD CONSTRAINT fk_rails_efccadc4ec FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -15,10 +15,18 @@ See also:
Start a new export.
The endpoint also accepts an `upload` parameter. This parameter is a hash. It contains
all the necessary information to upload the exported project to a web server or
to any S3-compatible platform. At the moment we only support binary
data file uploads to the final server.
The endpoint also accepts an `upload` hash parameter. It contains all the necessary information to upload the exported
project to a web server or to any S3-compatible platform. For exports, GitLab:
- Only supports binary data file uploads to the final server.
- Sends the `Content-Type: application/gzip` header with upload requests. Ensure that your pre-signed URL includes this
as part of the signature.
- Can take some time to complete the project export process. Make sure the upload URL doesn't have a short expiration
time and is available throughout the export process.
- Administrators can modify the maximum export file size. By default, the maximum is unlimited (`0`). To change this,
edit `max_export_size` using either:
- [Application settings API](settings.md#change-application-settings)
- [GitLab UI](../user/admin_area/settings/account_and_limit_settings.md).
The `upload[url]` parameter is required if the `upload` parameter is present.
@ -46,20 +54,6 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
}
```
NOTE:
The upload request is sent with `Content-Type: application/gzip` header. Ensure that your pre-signed URL includes this as part of the signature.
NOTE:
The project export process may take some time to complete. Make sure the
upload URL doesn't have a short expiration time and is available thought
the export process.
NOTE:
As an administrator, you can modify the maximum export file size. By default,
it is set to `0`, for unlimited. To change this value, edit `max_export_size`
in the [Application settings API](settings.md#change-application-settings)
or the [Admin UI](../user/admin_area/settings/account_and_limit_settings.md).
## Export status
Get the status of export.

View File

@ -258,3 +258,27 @@ pages-job:
script:
- 'curl --header "PRIVATE-TOKEN: ${PRIVATE_TOKEN}" "https://gitlab.example.com/api/v4/projects"'
```
### Job does not fail when using `&&` in a script
If you use `&&` to combine two commands together in a single script line, the job
might return as successful, even if one of the commands failed. For example:
```yaml
job-does-not-fail:
script:
- invalid-command xyz && invalid-command abc
- echo $?
- echo "The job should have failed already, but this is executed unexpectedly."
```
The `&&` operator returns an exit code of `0` even though the two commands failed,
and the job continues to run. To force the script to exit when either command fails,
enclose the entire line in parentheses:
```yaml
job-fails:
script:
- (invalid-command xyz && invalid-command abc)
- echo "The job failed already, and this is not executed."
```

View File

@ -32,15 +32,18 @@ Amazon provides a managed Kubernetes service offering known as [Amazon Elastic K
## Available Infrastructure as Code for GitLab Cloud Native Hybrid
The [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/blob/main/README.md) is an effort made by GitLab to create a multi-cloud, multi-GitLab (Omnibus + Cloud Native Hybrid) toolkit to provision GitLab. GET is developed by GitLab developers and is open to community contributions. GET is where GitLab is investing its resources as the primary option for Infrastructure as Code, and is being actively used in production as a part of [GitLab Dedicated](../../subscriptions/gitlab_dedicated/index.md).
Read the [GitLab Environment Toolkit (GET) direction](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/blob/main/README.md#direction) to learn more about the project and where it is going.
The [AWS Quick Start for GitLab Cloud Native Hybrid on EKS](https://aws-quickstart.github.io/quickstart-eks-gitlab/) is developed by AWS, GitLab, and the community that contributes to AWS Quick Starts, whether directly to the GitLab Quick Start or to the underlying Quick Start dependencies GitLab inherits (for example, EKS Quick Start).
GET is recommended for most deployments. The AWS Quick Start can be used if the IaC language of choice is CloudFormation, integration with AWS services like Control Tower is desired, or preference for a UI-driven configuration experience or when any aspect in the below table is an overriding concern.
NOTE:
This automation is in **[Open Beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#open-beta)**. GitLab is working with AWS on resolving [the outstanding issues](https://github.com/aws-quickstart/quickstart-eks-gitlab/issues?q=is%3Aissue+is%3Aopen+%5BHL%5D) before it is fully released. You can subscribe to this issue to be notified of progress and release announcements: [AWS Quick Start for GitLab Cloud Native Hybrid on EKS Status: Beta](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues/11).<br><br>
The Beta version deploys Aurora PostgreSQL, but the release version will deploy Amazon RDS PostgreSQL due to [known issues](https://gitlab.com/gitlab-com/alliances/aws/public-tracker/-/issues?label_name%5B%5D=AWS+Known+Issue) with Aurora. All performance testing results will also be redone after this change has been made.
The [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/tree/main) is an effort made by GitLab to create a multi-cloud, multi-GitLab (Omnibus + Cloud Native Hybrid) toolkit to provision GitLab. GET is developed by GitLab developers and is open to community contributions.
It is helpful to review the [GitLab Environment Toolkit (GET) Issues](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/issues) to understand if any of them may affect your provisioning plans.
| | [AWS Quick Start for GitLab Cloud Native Hybrid on EKS](https://aws-quickstart.github.io/quickstart-eks-gitlab/) | [GitLab Environment Toolkit (GET)](https://gitlab.com/gitlab-org/gitlab-environment-toolkit) |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| Overview and Vision | [AWS Quick Start](https://aws.amazon.com/quickstart/) | [GitLab Environment Toolkit](https://gitlab.com/gitlab-org/gitlab-environment-toolkit/-/blob/main/README.md) |
@ -56,9 +59,11 @@ It is helpful to review the [GitLab Environment Toolkit (GET) Issues](https://gi
| Results in a Ready-to-Use instance | Yes | Manual Actions or <br />Supplemental IaC Required |
| **<u>Configuration Features</u>** | | |
| Can deploy Omnibus GitLab (non-Kubernetes) | No | Yes |
| Results in a self-healing Gitaly Cluster configuration | Yes | No |
| Can deploy Single Instance Omnibus GitLab (non-Kubernetes) | No | Yes |
| Complete Internal Encryption | 85%, Targeting 100% | Manual |
| AWS GovCloud Support | Yes | TBD |
| No Code Form-Based Deployment User Experience Available | Yes | No |
| Full IaC User Experience Available | Yes | Yes |
### Two and Three Zone High Availability

View File

@ -8,37 +8,13 @@ module Gitlab
# Entry that represents a Docker image.
#
class Image < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
include ::Gitlab::Config::Entry::Configurable
ALLOWED_KEYS = %i[name entrypoint ports pull_policy].freeze
LEGACY_ALLOWED_KEYS = %i[name entrypoint ports].freeze
include ::Gitlab::Ci::Config::Entry::Imageable
validations do
validates :config, hash_or_string: true
validates :config, allowed_keys: ALLOWED_KEYS, if: :ci_docker_image_pull_policy_enabled?
validates :config, allowed_keys: LEGACY_ALLOWED_KEYS, unless: :ci_docker_image_pull_policy_enabled?
validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
validates :name, type: String, presence: true
validates :entrypoint, array_of_strings: true, allow_nil: true
end
entry :ports, Entry::Ports,
description: 'Ports used to expose the image'
entry :pull_policy, Entry::PullPolicy,
description: 'Pull policy for the image'
attributes :ports, :pull_policy
def name
value[:name]
end
def entrypoint
value[:entrypoint]
validates :config, allowed_keys: IMAGEABLE_ALLOWED_KEYS,
if: :ci_docker_image_pull_policy_enabled?
validates :config, allowed_keys: IMAGEABLE_LEGACY_ALLOWED_KEYS,
unless: :ci_docker_image_pull_policy_enabled?
end
def value
@ -55,18 +31,6 @@ module Gitlab
{}
end
end
def with_image_ports?
opt(:with_image_ports)
end
def ci_docker_image_pull_policy_enabled?
::Feature.enabled?(:ci_docker_image_pull_policy)
end
def skip_config_hash_validation?
true
end
end
end
end

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Represents Imageable concern shared by Image and Service.
module Imageable
extend ActiveSupport::Concern
include ::Gitlab::Config::Entry::Attributable
include ::Gitlab::Config::Entry::Configurable
IMAGEABLE_ALLOWED_KEYS = %i[name entrypoint ports pull_policy].freeze
IMAGEABLE_LEGACY_ALLOWED_KEYS = %i[name entrypoint ports].freeze
included do
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, hash_or_string: true
validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
validates :name, type: String, presence: true
validates :entrypoint, array_of_strings: true, allow_nil: true
end
attributes :ports, :pull_policy
entry :ports, Entry::Ports,
description: 'Ports used to expose the image/service'
entry :pull_policy, Entry::PullPolicy,
description: 'Pull policy for the image/service'
end
def name
value[:name]
end
def entrypoint
value[:entrypoint]
end
def with_image_ports?
opt(:with_image_ports)
end
def ci_docker_image_pull_policy_enabled?
::Feature.enabled?(:ci_docker_image_pull_policy)
end
def skip_config_hash_validation?
true
end
end
end
end
end
end

View File

@ -7,41 +7,28 @@ module Gitlab
##
# Entry that represents a configuration of Docker service.
#
# TODO: remove duplication with Image superclass by defining a common
# Imageable concern.
# https://gitlab.com/gitlab-org/gitlab/issues/208774
class Service < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Ci::Config::Entry::Imageable
ALLOWED_KEYS = %i[name entrypoint command alias ports variables pull_policy].freeze
LEGACY_ALLOWED_KEYS = %i[name entrypoint command alias ports variables].freeze
ALLOWED_KEYS = %i[command alias variables].freeze
LEGACY_ALLOWED_KEYS = %i[command alias variables].freeze
validations do
validates :config, hash_or_string: true
validates :config, allowed_keys: ALLOWED_KEYS, if: :ci_docker_image_pull_policy_enabled?
validates :config, allowed_keys: LEGACY_ALLOWED_KEYS, unless: :ci_docker_image_pull_policy_enabled?
validates :config, disallowed_keys: %i[ports], unless: :with_image_ports?
validates :name, type: String, presence: true
validates :entrypoint, array_of_strings: true, allow_nil: true
validates :config, allowed_keys: ALLOWED_KEYS + IMAGEABLE_ALLOWED_KEYS,
if: :ci_docker_image_pull_policy_enabled?
validates :config, allowed_keys: LEGACY_ALLOWED_KEYS + IMAGEABLE_LEGACY_ALLOWED_KEYS,
unless: :ci_docker_image_pull_policy_enabled?
validates :command, array_of_strings: true, allow_nil: true
validates :alias, type: String, allow_nil: true
validates :alias, type: String, presence: true, unless: ->(record) { record.ports.blank? }
end
entry :ports, Entry::Ports,
description: 'Ports used to expose the service'
entry :pull_policy, Entry::PullPolicy,
description: 'Pull policy for the service'
entry :variables, ::Gitlab::Ci::Config::Entry::Variables,
description: 'Environment variables available for this service.',
inherit: false
attributes :ports, :pull_policy, :variables
attributes :variables
def alias
value[:alias]
@ -51,14 +38,6 @@ module Gitlab
value[:command]
end
def name
value[:name]
end
def entrypoint
value[:entrypoint]
end
def value
if string?
{ name: @config }
@ -70,18 +49,6 @@ module Gitlab
{}
end
end
def with_image_ports?
opt(:with_image_ports)
end
def ci_docker_image_pull_policy_enabled?
::Feature.enabled?(:ci_docker_image_pull_policy)
end
def skip_config_hash_validation?
true
end
end
end
end

View File

@ -324,6 +324,10 @@ metrics_dashboard_annotations: :gitlab_main
metrics_users_starred_dashboards: :gitlab_main
milestone_releases: :gitlab_main
milestones: :gitlab_main
ml_candidates: :gitlab_main
ml_experiments: :gitlab_main
ml_candidate_metrics: :gitlab_main
ml_candidate_params: :gitlab_main
namespace_admin_notes: :gitlab_main
namespace_aggregation_schedules: :gitlab_main
namespace_bans: :gitlab_main

View File

@ -44391,6 +44391,9 @@ msgstr ""
msgid "WorkItem|None"
msgstr ""
msgid "WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this task."
msgstr ""
msgid "WorkItem|Open"
msgstr ""

View File

@ -23,7 +23,11 @@ RSpec.describe 'Group variables', :js do
it_behaves_like 'variable list'
end
# TODO: Uncomment when the new graphQL app for variable settings
# is enabled.
# it_behaves_like 'variable list'
context 'with enabled ff `ci_variable_settings_graphql' do
before do
visit page_path
end
it_behaves_like 'variable list'
end
end

View File

@ -0,0 +1,183 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { resolvers } from '~/ci_variable_list/graphql/resolvers';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciGroupVariables from '~/ci_variable_list/components/ci_group_variables.vue';
import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
import addGroupVariable from '~/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
import deleteGroupVariable from '~/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
import updateGroupVariable from '~/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants';
import { mockGroupVariables, newVariable } from '../mocks';
jest.mock('~/flash');
Vue.use(VueApollo);
const mockProvide = {
endpoint: '/variables',
groupPath: '/namespace/group',
groupId: 1,
};
describe('Ci Group Variable list', () => {
let wrapper;
let mockApollo;
let mockVariables;
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCiTable = () => wrapper.findComponent(GlTable);
const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
// eslint-disable-next-line consistent-return
const createComponentWithApollo = async ({ isLoading = false } = {}) => {
const handlers = [[getGroupVariables, mockVariables]];
mockApollo = createMockApollo(handlers, resolvers);
wrapper = shallowMount(ciGroupVariables, {
provide: mockProvide,
apolloProvider: mockApollo,
stubs: { ciVariableSettings, ciVariableTable },
});
if (!isLoading) {
return waitForPromises();
}
};
beforeEach(() => {
mockVariables = jest.fn();
});
afterEach(() => {
wrapper.destroy();
});
describe('while queries are being fetch', () => {
beforeEach(() => {
createComponentWithApollo({ isLoading: true });
});
it('shows a loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
expect(findCiTable().exists()).toBe(false);
});
});
describe('when queries are resolved', () => {
describe('successfuly', () => {
beforeEach(async () => {
mockVariables.mockResolvedValue(mockGroupVariables);
await createComponentWithApollo();
});
it('passes down the expected environments as props', () => {
expect(findCiSettings().props('environments')).toEqual([]);
});
it('passes down the expected variables as props', () => {
expect(findCiSettings().props('variables')).toEqual(
mockGroupVariables.data.group.ciVariables.nodes,
);
});
it('createFlash was not called', () => {
expect(createFlash).not.toHaveBeenCalled();
});
});
describe('with an error for variables', () => {
beforeEach(async () => {
mockVariables.mockRejectedValue();
await createComponentWithApollo();
});
it('calls createFlash with the expected error message', () => {
expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText });
});
});
});
describe('mutations', () => {
beforeEach(async () => {
mockVariables.mockResolvedValue(mockGroupVariables);
await createComponentWithApollo();
});
it.each`
actionName | mutation | event
${'add'} | ${addGroupVariable} | ${'add-variable'}
${'update'} | ${updateGroupVariable} | ${'update-variable'}
${'delete'} | ${deleteGroupVariable} | ${'delete-variable'}
`(
'calls the right mutation when user performs $actionName variable',
async ({ event, mutation }) => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
await findCiSettings().vm.$emit(event, newVariable);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation,
variables: {
endpoint: mockProvide.endpoint,
fullPath: mockProvide.groupPath,
groupId: convertToGraphQLId('Group', mockProvide.groupId),
variable: newVariable,
},
});
},
);
it.each`
actionName | event | mutationName
${'add'} | ${'add-variable'} | ${'addGroupVariable'}
${'update'} | ${'update-variable'} | ${'updateGroupVariable'}
${'delete'} | ${'delete-variable'} | ${'deleteGroupVariable'}
`(
'throws with the specific graphql error if present when user performs $actionName variable',
async ({ event, mutationName }) => {
const graphQLErrorMessage = 'There is a problem with this graphQL action';
jest
.spyOn(wrapper.vm.$apollo, 'mutate')
.mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
await findCiSettings().vm.$emit(event, newVariable);
await nextTick();
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage });
},
);
it.each`
actionName | event
${'add'} | ${'add-variable'}
${'update'} | ${'update-variable'}
${'delete'} | ${'delete-variable'}
`(
'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
async ({ event }) => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
throw new Error();
});
await findCiSettings().vm.$emit(event, newVariable);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText });
},
);
});
});

View File

@ -1,4 +1,4 @@
import { variableTypes, instanceString } from '~/ci_variable_list/constants';
import { variableTypes, groupString, instanceString } from '~/ci_variable_list/constants';
export const devName = 'dev';
export const prodName = 'prod';
@ -82,22 +82,12 @@ export const mockProjectVariables = {
},
};
export const mockGroupEnvironments = {
data: {
group: {
__typename: 'Group',
id: 1,
environments: defaultEnvs,
},
},
};
export const mockGroupVariables = {
data: {
group: {
__typename: 'Group',
id: 1,
ciVariables: createDefaultVars(),
ciVariables: createDefaultVars({ kind: groupString }),
},
},
};

View File

@ -15,6 +15,10 @@ import {
ICON_GROUP,
ICON_SUBGROUP,
SCOPE_TOKEN_MAX_LENGTH,
IS_SEARCHING,
IS_NOT_FOCUSED,
IS_FOCUSED,
SEARCH_SHORTCUTS_MIN_CHARACTERS,
} from '~/header_search/constants';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import { ENTER_KEY } from '~/lib/utils/keys';
@ -170,6 +174,14 @@ describe('HeaderSearchApp', () => {
it(`should render the Dropdown Navigation Component`, () => {
expect(findDropdownKeyboardNavigation().exists()).toBe(true);
});
it(`should close the dropdown when press escape key`, async () => {
findHeaderSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: 27 }));
await nextTick();
expect(findHeaderSearchDropdown().exists()).toBe(false);
// only one event emmited from findHeaderSearchInput().vm.$emit('click');
expect(wrapper.emitted().expandSearchBar.length).toBe(1);
});
});
});
@ -245,6 +257,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
findHeaderSearchInput().vm.$emit('click');
});
it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
@ -263,47 +276,43 @@ describe('HeaderSearchApp', () => {
});
});
describe('form wrapper', () => {
describe('form', () => {
describe.each`
searchContext | search | searchOptions
${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]}
${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]}
${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS}
${null} | ${null} | ${[]}
`('', ({ searchContext, search, searchOptions }) => {
searchContext | search | searchOptions | isFocused
${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]} | ${true}
${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]} | ${true}
${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${false}
${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS} | ${true}
${null} | ${null} | ${[]} | ${true}
`('wrapper', ({ searchContext, search, searchOptions, isFocused }) => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
findHeaderSearchInput().vm.$emit('click');
if (isFocused) {
findHeaderSearchInput().vm.$emit('click');
}
});
const hasIcon = Boolean(searchContext?.group);
const isSearching = Boolean(search);
const isActive = Boolean(searchOptions.length > 0);
const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
it(`${hasIcon ? 'with' : 'without'} search context classes contain "${
hasIcon ? 'has-icon' : 'has-no-icon'
}"`, () => {
const iconClassRegex = hasIcon ? 'has-icon' : 'has-no-icon';
expect(findHeaderSearchForm().classes()).toContain(iconClassRegex);
it(`classes ${isSearching ? 'contain' : 'do not contain'} "${IS_SEARCHING}"`, () => {
if (isSearching) {
expect(findHeaderSearchForm().classes()).toContain(IS_SEARCHING);
return;
}
if (!isSearching) {
expect(findHeaderSearchForm().classes()).not.toContain(IS_SEARCHING);
}
});
it(`${isSearching ? 'with' : 'without'} search string classes contain "${
isSearching ? 'is-searching' : 'is-not-searching'
it(`classes ${isSearching ? 'contain' : 'do not contain'} "${
isFocused ? IS_FOCUSED : IS_NOT_FOCUSED
}"`, () => {
const iconClassRegex = isSearching ? 'is-searching' : 'is-not-searching';
expect(findHeaderSearchForm().classes()).toContain(iconClassRegex);
});
it(`${isActive ? 'with' : 'without'} search results classes contain "${
isActive ? 'is-active' : 'is-not-active'
}"`, () => {
const iconClassRegex = isActive ? 'is-active' : 'is-not-active';
expect(findHeaderSearchForm().classes()).toContain(iconClassRegex);
expect(findHeaderSearchForm().classes()).toContain(
isFocused ? IS_FOCUSED : IS_NOT_FOCUSED,
);
});
});
});
@ -323,6 +332,7 @@ describe('HeaderSearchApp', () => {
searchOptions: () => searchOptions,
},
);
findHeaderSearchInput().vm.$emit('click');
});
it(`icon for data set type "${searchOptions[0]?.html_id}" ${

View File

@ -164,7 +164,7 @@ describe('RepoTab', () => {
await wrapper.find('.multi-file-tab-close').trigger('click');
expect(tab.opened).toBeFalsy();
expect(tab.opened).toBe(false);
expect(wrapper.vm.$store.state.changedFiles).toHaveLength(1);
});
@ -180,7 +180,7 @@ describe('RepoTab', () => {
await wrapper.find('.multi-file-tab-close').trigger('click');
expect(tab.opened).toBeFalsy();
expect(tab.opened).toBe(false);
});
});
});

View File

@ -128,7 +128,7 @@ describe('SignInOauthButton', () => {
});
it('does not emit `sign-in` event', () => {
expect(wrapper.emitted('sign-in')).toBeFalsy();
expect(wrapper.emitted('sign-in')).toBeUndefined();
});
it('sets `loading` prop of button to `false`', () => {
@ -179,7 +179,7 @@ describe('SignInOauthButton', () => {
});
it('emits `sign-in` event with user data', () => {
expect(wrapper.emitted('sign-in')[0]).toBeTruthy();
expect(wrapper.emitted('sign-in')).toHaveLength(1);
});
});
@ -200,7 +200,7 @@ describe('SignInOauthButton', () => {
});
it('does not emit `sign-in` event', () => {
expect(wrapper.emitted('sign-in')).toBeFalsy();
expect(wrapper.emitted('sign-in')).toBeUndefined();
});
it('sets `loading` prop of button to `false`', () => {

View File

@ -4,7 +4,6 @@ import { GlEmptyState } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { stripTypenames } from 'helpers/graphql_helpers';
import component from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue';
import TagsListRow from '~/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue';
@ -96,8 +95,8 @@ describe('Tags List', () => {
it('binds the correct props', () => {
expect(findRegistryList().props()).toMatchObject({
title: '2 tags',
pagination: stripTypenames(tagsPageInfo),
items: stripTypenames(tags),
pagination: tagsPageInfo,
items: tags,
idProperty: 'name',
});
});

View File

@ -210,12 +210,15 @@ exports[`releases/util.js convertOneReleaseForEditingGraphQLResponse matches sna
Object {
"data": Object {
"_links": Object {
"__typename": "ReleaseLinks",
"self": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
"selfUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
},
"assets": Object {
"count": undefined,
"links": Array [
Object {
"__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/awesome-app-3",
"id": "gid://gitlab/Releases::Link/13",
"linkType": "image",
@ -223,6 +226,7 @@ Object {
"url": "https://example.com/image",
},
Object {
"__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/awesome-app-2",
"id": "gid://gitlab/Releases::Link/12",
"linkType": "package",
@ -230,6 +234,7 @@ Object {
"url": "https://example.com/package",
},
Object {
"__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/awesome-app-1",
"id": "gid://gitlab/Releases::Link/11",
"linkType": "runbook",
@ -237,6 +242,7 @@ Object {
"url": "http://localhost/releases-namespace/releases-project/runbook",
},
Object {
"__typename": "ReleaseAssetLink",
"directAssetPath": "/binaries/linux-amd64",
"id": "gid://gitlab/Releases::Link/10",
"linkType": "other",
@ -246,22 +252,31 @@ Object {
],
"sources": Array [],
},
"author": undefined,
"description": "Best. Release. **Ever.** :rocket:",
"evidences": Array [],
"milestones": Array [
Object {
"__typename": "Milestone",
"id": "gid://gitlab/Milestone/123",
"issueStats": Object {},
"stats": undefined,
"title": "12.3",
"webPath": undefined,
"webUrl": undefined,
},
Object {
"__typename": "Milestone",
"id": "gid://gitlab/Milestone/124",
"issueStats": Object {},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": undefined,
},
],
"name": "The first release",
"releasedAt": "2018-12-10T00:00:00.000Z",
"releasedAt": 2018-12-10T00:00:00.000Z,
"tagName": "v1.1",
"tagPath": "/releases-namespace/releases-project/-/tags/v1.1",
},

View File

@ -7,7 +7,6 @@ import {
convertAllReleasesGraphQLResponse,
convertOneReleaseGraphQLResponse,
} from '~/releases/util';
import { stripTypenames } from 'helpers/graphql_helpers';
describe('releases/util.js', () => {
describe('convertGraphQLRelease', () => {
@ -137,7 +136,7 @@ describe('releases/util.js', () => {
describe('convertOneReleaseForEditingGraphQLResponse', () => {
it('matches snapshot', () => {
expect(
stripTypenames(convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse)),
convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse),
).toMatchSnapshot();
});
});

View File

@ -5,7 +5,6 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { stripTypenames } from 'helpers/graphql_helpers';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
@ -311,9 +310,7 @@ describe('WorkItemAssignees component', () => {
findAssignSelfButton().vm.$emit('click', new MouseEvent('click'));
await nextTick();
expect(findTokenSelector().props('selectedTokens')).toMatchObject([
stripTypenames(currentUser),
]);
expect(findTokenSelector().props('selectedTokens')).toMatchObject([currentUser]);
expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
input: {
id: workItemId,
@ -330,9 +327,7 @@ describe('WorkItemAssignees component', () => {
await waitForPromises();
expect(findTokenSelector().props('dropdownItems')[0]).toEqual(
expect.objectContaining({
...stripTypenames(currentUserResponse.data.currentUser),
}),
expect.objectContaining(currentUserResponse.data.currentUser),
);
});

View File

@ -247,6 +247,9 @@ describe('WorkItemDetail component', () => {
variant: 'warning',
icon: 'eye-slash',
});
expect(confidentialBadge.attributes('title')).toBe(
'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this task.',
);
expect(confidentialBadge.text()).toBe('Confidential');
});

View File

@ -1,12 +1,8 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'support/helpers/stubbed_feature'
require 'support/helpers/stub_feature_flags'
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Image do
include StubFeatureFlags
before do
stub_feature_flags(ci_docker_image_pull_policy: true)

View File

@ -0,0 +1,81 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Imageable do
let(:node_class) do
Class.new(::Gitlab::Config::Entry::Node) do
include ::Gitlab::Ci::Config::Entry::Imageable
validations do
validates :config, allowed_keys: ::Gitlab::Ci::Config::Entry::Imageable::IMAGEABLE_ALLOWED_KEYS
end
def self.name
'node'
end
def value
if string?
{ name: @config }
elsif hash?
{
name: @config[:name]
}.compact
else
{}
end
end
end
end
subject(:entry) { node_class.new(config) }
before do
entry.compose!
end
context 'when entry value is correct' do
let(:config) { 'image:1.0' }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
let(:config) { ['image:1.0'] }
describe '#errors' do
it 'saves errors' do
expect(entry.errors.first)
.to match /config should be a hash or a string/
end
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
context 'when unexpected key is specified' do
let(:config) { { name: 'image:1.0', non_existing: 'test' } }
describe '#errors' do
it 'saves errors' do
expect(entry.errors.first)
.to match /config contains unknown keys: non_existing/
end
end
describe '#valid?' do
it 'is not valid' do
expect(entry).not_to be_valid
end
end
end
end

View File

@ -2,7 +2,6 @@
require 'fast_spec_helper'
require 'gitlab_chronic_duration'
require 'support/helpers/stub_feature_flags'
require_dependency 'active_model'
RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do

View File

@ -1,12 +1,8 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'support/helpers/stubbed_feature'
require 'support/helpers/stub_feature_flags'
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Service do
include StubFeatureFlags
before do
stub_feature_flags(ci_docker_image_pull_policy: true)
entry.compose!

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ml::CandidateMetric do
describe 'associations' do
it { is_expected.to belong_to(:candidate) }
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ml::CandidateParam do
describe 'associations' do
it { is_expected.to belong_to(:candidate) }
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ml::Candidate do
describe 'associations' do
it { is_expected.to belong_to(:experiment) }
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:params) }
it { is_expected.to have_many(:metrics) }
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ml::Experiment do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:candidates) }
end
end

View File

@ -70,9 +70,12 @@
keywords:
- '*.png'
- '*bundler-audit*'
- '**/merge_requests/**'
- '/ee/app/services/audit_events/*'
- '/ee/config/feature_flags/development/auditor_group_runner_access.yml'
- '/ee/spec/services/audit_events/*'
- '/ee/spec/services/ci/*'
- '/ee/spec/services/personal_access_tokens/*'
- '/qa/**/*'
patterns:
- '%{keyword}'