Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-08-17 18:11:57 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-17 18:11:57 +0300
commit5ff5438a0674c1e8217f78d2000c61c9d550c503 (patch)
tree087474c8b10d24282e30087e1048a32f4591b79d
parent458b945df3652f3f42f3665ea2e39e745c8b05c6 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/CODEOWNERS9
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue104
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql30
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql30
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql30
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql17
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/resolvers.js48
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js7
-rw-r--r--app/assets/javascripts/header_search/components/app.vue47
-rw-r--r--app/assets/javascripts/header_search/constants.js4
-rw-r--r--app/assets/javascripts/header_search/index.js11
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue12
-rw-r--r--app/assets/javascripts/work_items/constants.js3
-rw-r--r--app/assets/stylesheets/pages/search.scss31
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss8
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb3
-rw-r--r--app/models/ml.rb6
-rw-r--r--app/models/ml/candidate.rb12
-rw-r--r--app/models/ml/candidate_metric.rb10
-rw-r--r--app/models/ml/candidate_param.rb10
-rw-r--r--app/models/ml/experiment.rb12
-rw-r--r--db/docs/ml_candidate_metrics.yml10
-rw-r--r--db/docs/ml_candidate_params.yml10
-rw-r--r--db/docs/ml_candidates.yml10
-rw-r--r--db/docs/ml_experiments.yml10
-rw-r--r--db/migrate/20220811092243_create_ml_experiments.rb18
-rw-r--r--db/migrate/20220811092244_create_ml_candidates.rb16
-rw-r--r--db/migrate/20220811092245_create_ml_candidate_params.rb14
-rw-r--r--db/migrate/20220811092246_create_ml_candidate_metrics.rb16
-rw-r--r--db/migrate/20220811092251_add_ml_candidates_reference_to_experiment.rb15
-rw-r--r--db/migrate/20220811092253_add_ml_experiments_reference_to_project.rb15
-rw-r--r--db/schema_migrations/202208110922431
-rw-r--r--db/schema_migrations/202208110922441
-rw-r--r--db/schema_migrations/202208110922451
-rw-r--r--db/schema_migrations/202208110922461
-rw-r--r--db/schema_migrations/202208110922511
-rw-r--r--db/schema_migrations/202208110922531
-rw-r--r--db/structure.sql131
-rw-r--r--doc/api/project_import_export.md30
-rw-r--r--doc/ci/yaml/script.md24
-rw-r--r--doc/install/aws/gitlab_hybrid_on_aws.md13
-rw-r--r--lib/gitlab/ci/config/entry/image.rb46
-rw-r--r--lib/gitlab/ci/config/entry/imageable.rb61
-rw-r--r--lib/gitlab/ci/config/entry/service.rb49
-rw-r--r--lib/gitlab/database/gitlab_schemas.yml4
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/features/group_variables_spec.rb10
-rw-r--r--spec/frontend/ci_variable_list/components/ci_group_variables_spec.js183
-rw-r--r--spec/frontend/ci_variable_list/mocks.js14
-rw-r--r--spec/frontend/header_search/components/app_spec.js72
-rw-r--r--spec/frontend/ide/components/repo_tab_spec.js4
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js6
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js5
-rw-r--r--spec/frontend/releases/__snapshots__/util_spec.js.snap17
-rw-r--r--spec/frontend/releases/util_spec.js3
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js9
-rw-r--r--spec/frontend/work_items/pages/work_item_detail_spec.js3
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/entry/imageable_spec.rb81
-rw-r--r--spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb6
-rw-r--r--spec/models/ml/candidate_metric_spec.rb9
-rw-r--r--spec/models/ml/candidate_param_spec.rb9
-rw-r--r--spec/models/ml/candidate_spec.rb12
-rw-r--r--spec/models/ml/experiment_spec.rb11
-rw-r--r--tooling/config/CODEOWNERS.yml3
68 files changed, 1173 insertions, 240 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index 6f858f0e0a6..6019fe636a8 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -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
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
new file mode 100644
index 00000000000..3af83ffa8ed
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
@@ -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>
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
new file mode 100644
index 00000000000..f8e4dc55fa4
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
@@ -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
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
new file mode 100644
index 00000000000..310e4a6e551
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
@@ -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
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
new file mode 100644
index 00000000000..5291942eb87
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
@@ -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
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
new file mode 100644
index 00000000000..c6dd6d4faaf
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
@@ -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
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js b/app/assets/javascripts/ci_variable_list/graphql/resolvers.js
index 7b57e97a4b8..be7e3f88cfd 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js
+++ b/app/assets/javascripts/ci_variable_list/graphql/resolvers.js
@@ -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 });
},
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index 713a453561e..a74af8aed12 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -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);
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index 72fec17ac9d..f4b939fb20f 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -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" />
diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js
index a026386b2bd..3a20fb0216d 100644
--- a/app/assets/javascripts/header_search/constants.js
+++ b/app/assets/javascripts/header_search/constants.js
@@ -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';
diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js
index b2c505d569f..f6f5c6a14fa 100644
--- a/app/assets/javascripts/header_search/index.js
+++ b/app/assets/javascripts/header_search/index.js
@@ -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');
},
},
});
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index 6748a62e777..9cce6723bf7 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -68,7 +68,7 @@ export default {
}),
tableCell({
key: 'created_at',
- label: __('Date'),
+ label: __('Start date'),
}),
tableCell({
key: 'status',
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 8e862aa8b32..a5580c14a7a 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -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"
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index f1fc21b74f4..a2aea3cd327 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -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 = {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 012407cf09c..6c909b8d9fa 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -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 {
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index a86e489439f..ffe4d5dde9d 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -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);
}
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index 2b6221a6c87..042e21cebd6 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -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);
}
}
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index b1afac1f1c7..e164a834519 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -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
diff --git a/app/models/ml.rb b/app/models/ml.rb
new file mode 100644
index 00000000000..e426ce851eb
--- /dev/null
+++ b/app/models/ml.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+module Ml
+ def self.table_name_prefix
+ 'ml_'
+ end
+end
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
new file mode 100644
index 00000000000..e181217f01c
--- /dev/null
+++ b/app/models/ml/candidate.rb
@@ -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
diff --git a/app/models/ml/candidate_metric.rb b/app/models/ml/candidate_metric.rb
new file mode 100644
index 00000000000..e03a8b83ee6
--- /dev/null
+++ b/app/models/ml/candidate_metric.rb
@@ -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
diff --git a/app/models/ml/candidate_param.rb b/app/models/ml/candidate_param.rb
new file mode 100644
index 00000000000..cbdddcc8a1a
--- /dev/null
+++ b/app/models/ml/candidate_param.rb
@@ -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
diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb
new file mode 100644
index 00000000000..7ef9c70ba7e
--- /dev/null
+++ b/app/models/ml/experiment.rb
@@ -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
diff --git a/db/docs/ml_candidate_metrics.yml b/db/docs/ml_candidate_metrics.yml
new file mode 100644
index 00000000000..b0d9ed13489
--- /dev/null
+++ b/db/docs/ml_candidate_metrics.yml
@@ -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'
diff --git a/db/docs/ml_candidate_params.yml b/db/docs/ml_candidate_params.yml
new file mode 100644
index 00000000000..01903b66108
--- /dev/null
+++ b/db/docs/ml_candidate_params.yml
@@ -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'
diff --git a/db/docs/ml_candidates.yml b/db/docs/ml_candidates.yml
new file mode 100644
index 00000000000..c1f7f622350
--- /dev/null
+++ b/db/docs/ml_candidates.yml
@@ -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'
diff --git a/db/docs/ml_experiments.yml b/db/docs/ml_experiments.yml
new file mode 100644
index 00000000000..ea5edc9569c
--- /dev/null
+++ b/db/docs/ml_experiments.yml
@@ -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'
diff --git a/db/migrate/20220811092243_create_ml_experiments.rb b/db/migrate/20220811092243_create_ml_experiments.rb
new file mode 100644
index 00000000000..a357d62133c
--- /dev/null
+++ b/db/migrate/20220811092243_create_ml_experiments.rb
@@ -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
diff --git a/db/migrate/20220811092244_create_ml_candidates.rb b/db/migrate/20220811092244_create_ml_candidates.rb
new file mode 100644
index 00000000000..fe9fc293b03
--- /dev/null
+++ b/db/migrate/20220811092244_create_ml_candidates.rb
@@ -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
diff --git a/db/migrate/20220811092245_create_ml_candidate_params.rb b/db/migrate/20220811092245_create_ml_candidate_params.rb
new file mode 100644
index 00000000000..55fc1cbe7af
--- /dev/null
+++ b/db/migrate/20220811092245_create_ml_candidate_params.rb
@@ -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
diff --git a/db/migrate/20220811092246_create_ml_candidate_metrics.rb b/db/migrate/20220811092246_create_ml_candidate_metrics.rb
new file mode 100644
index 00000000000..a4d417f270e
--- /dev/null
+++ b/db/migrate/20220811092246_create_ml_candidate_metrics.rb
@@ -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
diff --git a/db/migrate/20220811092251_add_ml_candidates_reference_to_experiment.rb b/db/migrate/20220811092251_add_ml_candidates_reference_to_experiment.rb
new file mode 100644
index 00000000000..a3be644552c
--- /dev/null
+++ b/db/migrate/20220811092251_add_ml_candidates_reference_to_experiment.rb
@@ -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
diff --git a/db/migrate/20220811092253_add_ml_experiments_reference_to_project.rb b/db/migrate/20220811092253_add_ml_experiments_reference_to_project.rb
new file mode 100644
index 00000000000..4fd832dfe14
--- /dev/null
+++ b/db/migrate/20220811092253_add_ml_experiments_reference_to_project.rb
@@ -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
diff --git a/db/schema_migrations/20220811092243 b/db/schema_migrations/20220811092243
new file mode 100644
index 00000000000..6640e5f1a65
--- /dev/null
+++ b/db/schema_migrations/20220811092243
@@ -0,0 +1 @@
+211eda22a78d14aaaf86345d3e33b852ba22a7dc9e41d9d683d58f162a7bdcc7 \ No newline at end of file
diff --git a/db/schema_migrations/20220811092244 b/db/schema_migrations/20220811092244
new file mode 100644
index 00000000000..824d936a09c
--- /dev/null
+++ b/db/schema_migrations/20220811092244
@@ -0,0 +1 @@
+f871847fbd494e31f13cf2fb87a1b8e9fc47c44e7f0ec9cf37f2084d19b9bf5f \ No newline at end of file
diff --git a/db/schema_migrations/20220811092245 b/db/schema_migrations/20220811092245
new file mode 100644
index 00000000000..09bd431d928
--- /dev/null
+++ b/db/schema_migrations/20220811092245
@@ -0,0 +1 @@
+0c856ce8170e4b864578f1bcb89d8930d8c1952e92356965a98e057521456968 \ No newline at end of file
diff --git a/db/schema_migrations/20220811092246 b/db/schema_migrations/20220811092246
new file mode 100644
index 00000000000..64d3153b833
--- /dev/null
+++ b/db/schema_migrations/20220811092246
@@ -0,0 +1 @@
+17bcb2fddd6331cbcec505e8094d1a400b7c3fd8b18897697aa9868689147cd7 \ No newline at end of file
diff --git a/db/schema_migrations/20220811092251 b/db/schema_migrations/20220811092251
new file mode 100644
index 00000000000..61e2afaa74a
--- /dev/null
+++ b/db/schema_migrations/20220811092251
@@ -0,0 +1 @@
+4ea4bc7e6f88561553b19c7bf4992561772506cf532cf569241a536f69e19b7f \ No newline at end of file
diff --git a/db/schema_migrations/20220811092253 b/db/schema_migrations/20220811092253
new file mode 100644
index 00000000000..fc8ad6918e9
--- /dev/null
+++ b/db/schema_migrations/20220811092253
@@ -0,0 +1 @@
+6a6eed069e051786a925b40469e7b53a563f99f0c6bfb810058511d3de8b0923 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index c2276aebc87..b055d831ce6 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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;
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index caecdff6e75..49a9b68227d 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -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.
diff --git a/doc/ci/yaml/script.md b/doc/ci/yaml/script.md
index 4bffcbca1cc..f1cdcf57e64 100644
--- a/doc/ci/yaml/script.md
+++ b/doc/ci/yaml/script.md
@@ -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."
+```
diff --git a/doc/install/aws/gitlab_hybrid_on_aws.md b/doc/install/aws/gitlab_hybrid_on_aws.md
index bc811cab3bf..b7a01cf61f4 100644
--- a/doc/install/aws/gitlab_hybrid_on_aws.md
+++ b/doc/install/aws/gitlab_hybrid_on_aws.md
@@ -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
diff --git a/lib/gitlab/ci/config/entry/image.rb b/lib/gitlab/ci/config/entry/image.rb
index 96ac959a3f4..613f7ff3370 100644
--- a/lib/gitlab/ci/config/entry/image.rb
+++ b/lib/gitlab/ci/config/entry/image.rb
@@ -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
diff --git a/lib/gitlab/ci/config/entry/imageable.rb b/lib/gitlab/ci/config/entry/imageable.rb
new file mode 100644
index 00000000000..f045ee3d549
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/imageable.rb
@@ -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
diff --git a/lib/gitlab/ci/config/entry/service.rb b/lib/gitlab/ci/config/entry/service.rb
index 1a35f7de6cf..0e19447dff8 100644
--- a/lib/gitlab/ci/config/entry/service.rb
+++ b/lib/gitlab/ci/config/entry/service.rb
@@ -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
diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml
index 06287126978..d05eee7d6e6 100644
--- a/lib/gitlab/database/gitlab_schemas.yml
+++ b/lib/gitlab/database/gitlab_schemas.yml
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 93b74d3d54c..8e7d31fd88e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index 9af9baeb5bb..ab24162ad5a 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -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
diff --git a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
new file mode 100644
index 00000000000..e45656acfd8
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
@@ -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 });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js
index 07dc7a8c91f..89ba77858dc 100644
--- a/spec/frontend/ci_variable_list/mocks.js
+++ b/spec/frontend/ci_variable_list/mocks.js
@@ -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 }),
},
},
};
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index d89218f5542..6a138f9a247 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -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}" ${
diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js
index 0dda176b47c..b26edc5a85b 100644
--- a/spec/frontend/ide/components/repo_tab_spec.js
+++ b/spec/frontend/ide/components/repo_tab_spec.js
@@ -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);
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
index 8f79c74368f..ed0abaaf576 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
@@ -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`', () => {
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
index ef6c4a1fa32..b163557618e 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_spec.js
@@ -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',
});
});
diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap
index 2a12d945792..55e3dda60a0 100644
--- a/spec/frontend/releases/__snapshots__/util_spec.js.snap
+++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap
@@ -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",
},
diff --git a/spec/frontend/releases/util_spec.js b/spec/frontend/releases/util_spec.js
index dfea3fc0037..055c8e8b39f 100644
--- a/spec/frontend/releases/util_spec.js
+++ b/spec/frontend/releases/util_spec.js
@@ -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();
});
});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index ee69e3bee83..f0ef8aee7a9 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -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),
);
});
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js
index 5fef151be81..823981df880 100644
--- a/spec/frontend/work_items/pages/work_item_detail_spec.js
+++ b/spec/frontend/work_items/pages/work_item_detail_spec.js
@@ -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');
});
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index 0fa6d4f8804..6121c28070f 100644
--- a/spec/lib/gitlab/ci/config/entry/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -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)
diff --git a/spec/lib/gitlab/ci/config/entry/imageable_spec.rb b/spec/lib/gitlab/ci/config/entry/imageable_spec.rb
new file mode 100644
index 00000000000..88f8e260611
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/imageable_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
index 10b4cba3bbb..c85fe366da6 100644
--- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index 3c000fd09ed..821ab442d61 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -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!
diff --git a/spec/models/ml/candidate_metric_spec.rb b/spec/models/ml/candidate_metric_spec.rb
new file mode 100644
index 00000000000..5ee6030fb8e
--- /dev/null
+++ b/spec/models/ml/candidate_metric_spec.rb
@@ -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
diff --git a/spec/models/ml/candidate_param_spec.rb b/spec/models/ml/candidate_param_spec.rb
new file mode 100644
index 00000000000..ff38e471219
--- /dev/null
+++ b/spec/models/ml/candidate_param_spec.rb
@@ -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
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
new file mode 100644
index 00000000000..a48e291fa55
--- /dev/null
+++ b/spec/models/ml/candidate_spec.rb
@@ -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
diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb
new file mode 100644
index 00000000000..dca5280a8fe
--- /dev/null
+++ b/spec/models/ml/experiment_spec.rb
@@ -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
diff --git a/tooling/config/CODEOWNERS.yml b/tooling/config/CODEOWNERS.yml
index 9a3a7e8e5be..71818b67ab1 100644
--- a/tooling/config/CODEOWNERS.yml
+++ b/tooling/config/CODEOWNERS.yml
@@ -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}'