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>2021-01-13 15:10:27 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-01-13 15:10:27 +0300
commit39c1496527de559d5d3a5c3b53d11575f435a4dc (patch)
tree51ed818b49752bb27d8bc7a13e9efcb3e5192c1f
parentab9c1dbb2dc0e591a6ce4466e15766d99f4abf4b (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml10
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml11
-rw-r--r--app/assets/javascripts/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql5
-rw-r--r--app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql7
-rw-r--r--app/assets/javascripts/artifacts_settings/index.js32
-rw-r--r--app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue99
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue21
-rw-r--r--app/assets/javascripts/boards/graphql/board_create.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/board_update.mutation.graphql4
-rw-r--r--app/assets/javascripts/ide/lib/languages/hcl.js2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue7
-rw-r--r--app/assets/javascripts/merge_request/components/status_box.vue31
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js4
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/controllers/jira_connect/app_descriptor_controller.rb14
-rw-r--r--app/controllers/metrics_controller.rb10
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/graphql/resolvers/ci/config_resolver.rb55
-rw-r--r--app/graphql/types/ci/config/job_restriction_type.rb15
-rw-r--r--app/graphql/types/ci/config/job_type.rb31
-rw-r--r--app/models/commit.rb6
-rw-r--r--app/models/merge_request.rb20
-rw-r--r--app/serializers/merge_request_poll_cached_widget_entity.rb8
-rw-r--r--app/services/feature_flags/base_service.rb15
-rw-r--r--app/views/groups/group_members/index.html.haml34
-rw-r--r--app/views/profiles/two_factor_auths/_codes.html.haml17
-rw-r--r--app/views/profiles/two_factor_auths/create.html.haml4
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml9
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml9
-rw-r--r--app/views/projects/project_members/index.html.haml36
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml11
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/jira_connect/sync_feature_flags_worker.rb24
-rw-r--r--changelogs/unreleased/282299-grouping-swimlanes-choice-should-persist-after-board-has-been-edit.yml5
-rw-r--r--changelogs/unreleased/35824-Indicate-Reverted-MRs-in-MR-Header.yml5
-rw-r--r--changelogs/unreleased/lm-add-new-jobs-fields-graphql.yml5
-rw-r--r--changelogs/unreleased/pb-keep-latest-artifact-setting-ui.yml5
-rw-r--r--config/feature_flags/development/ci_pipeline_open_merge_requests.yml2
-rw-r--r--config/feature_flags/development/jira_sync_feature_flags.yml (renamed from config/feature_flags/development/vue_2fa_recovery_codes.yml)12
-rw-r--r--config/feature_flags/development/unified_diff_components.yml2
-rw-r--r--config/routes.rb5
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql65
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json211
-rw-r--r--doc/api/graphql/reference/index.md23
-rw-r--r--doc/ci/pipelines/job_artifacts.md17
-rw-r--r--doc/ci/variables/predefined_variables.md2
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/pipelines.md23
-rw-r--r--doc/development/profiling.md56
-rw-r--r--doc/user/project/merge_requests/revert_changes.md4
-rw-r--r--lib/atlassian/jira_connect/client.rb16
-rw-r--r--lib/atlassian/jira_connect/serializers/feature_flag_entity.rb83
-rw-r--r--lib/gitlab/ci/features.rb2
-rw-r--r--lib/gitlab/ci/lint.rb3
-rw-r--r--lib/gitlab/metrics/system.rb14
-rw-r--r--locale/gitlab.pot33
-rw-r--r--package.json2
-rw-r--r--spec/controllers/metrics_controller_spec.rb75
-rw-r--r--spec/features/admin/admin_groups_spec.rb1
-rw-r--r--spec/features/groups/members/manage_groups_spec.rb1
-rw-r--r--spec/features/groups/members/manage_members_spec.rb28
-rw-r--r--spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb1
-rw-r--r--spec/features/projects/members/group_members_spec.rb1
-rw-r--r--spec/features/projects/members/list_spec.rb1
-rw-r--r--spec/features/users/login_spec.rb67
-rw-r--r--spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap29
-rw-r--r--spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js62
-rw-r--r--spec/frontend/boards/components/board_form_spec.js57
-rw-r--r--spec/frontend/ide/lib/languages/hcl_spec.js36
-rw-r--r--spec/frontend/merge_request/components/status_box_spec.js20
-rw-r--r--spec/graphql/resolvers/ci/config_resolver_spec.rb16
-rw-r--r--spec/graphql/types/ci/config/job_restriction_type_spec.rb13
-rw-r--r--spec/graphql/types/ci/config/job_type_spec.rb9
-rw-r--r--spec/lib/atlassian/jira_connect/client_spec.rb71
-rw-r--r--spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb85
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb34
-rw-r--r--spec/models/commit_spec.rb21
-rw-r--r--spec/models/merge_request_spec.rb79
-rw-r--r--spec/requests/api/graphql/ci/config_spec.rb142
-rw-r--r--spec/services/feature_flags/create_service_spec.rb24
-rw-r--r--spec/services/feature_flags/update_service_spec.rb24
-rw-r--r--spec/support/atlassian/jira_connect/schemata.rb110
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci_includes.yml26
-rw-r--r--spec/support/matchers/schema_matcher.rb8
-rw-r--r--spec/workers/jira_connect/sync_feature_flags_worker_spec.rb60
-rw-r--r--yarn.lock2
90 files changed, 1821 insertions, 456 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 65fc66bddca..7f8dfa900ca 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -300,6 +300,16 @@ rspec system pg11 minimal:
- .minimal-rspec-tests
- .rails:rules:ee-and-foss-system:minimal
+# Dedicated job to test DB library code against PG12.
+# Note that these are already tested against PG11 in the `rspec unit pg11` / `rspec-ee unit pg11` jobs.
+rspec db-library-code pg12:
+ extends:
+ - .rspec-base-pg12
+ - .rails:rules:ee-and-foss-db-library-code
+ script:
+ - *base-script
+ - rspec_simple_job "-- spec/lib/gitlab/database/ spec/support/helpers/database/ ee/spec/lib/gitlab/database/ ee/spec/lib/ee/gitlab/database_spec.rb"
+
rspec fast_spec_helper:
extends:
- .rspec-base-pg11
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 9ea5538f643..5e8cdf0daaf 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -155,9 +155,15 @@
- "{,ee/}{,spec/}lib/{,ee/}gitlab/database{,_spec}.rb"
- "{,ee/}{,spec/}lib/{,ee/}gitlab/background_migration/**/*"
- "{,ee/}{,spec/}lib/{,ee/}gitlab/background_migration{,_spec}.rb"
+ - "{,ee/}spec/support/helpers/database/**/*"
- "config/prometheus/common_metrics.yml" # Used by Gitlab::DatabaseImporters::CommonMetrics::Importer
- "{,ee/}app/models/project_statistics.rb" # Used to calculate sizes in migration specs
+.db-library-patterns: &db-library-patterns
+ - "{,ee/}{,spec/}lib/{,ee/}gitlab/database/**/*"
+ - "{,ee/}{,spec/}lib/{,ee/}gitlab/database{,_spec}.rb"
+ - "{,ee/}spec/support/helpers/database/**/*"
+
.backstage-patterns: &backstage-patterns
- "Dangerfile"
- "danger/**/*"
@@ -775,6 +781,11 @@
- <<: *if-merge-request-title-as-if-foss
changes: *code-backstage-patterns
+.rails:rules:ee-and-foss-db-library-code:
+ rules:
+ - changes: *db-library-patterns
+ - <<: *if-merge-request-title-run-all-rspec
+
.rails:rules:ee-mr-and-master-only:
rules:
- <<: *if-not-ee
diff --git a/app/assets/javascripts/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql b/app/assets/javascripts/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql
new file mode 100644
index 00000000000..d50fd665c16
--- /dev/null
+++ b/app/assets/javascripts/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql
@@ -0,0 +1,5 @@
+mutation updateKeepLatestArtifactProjectSetting($fullPath: ID!, $keepLatestArtifact: Boolean!) {
+ ciCdSettingsUpdate(input: { fullPath: $fullPath, keepLatestArtifact: $keepLatestArtifact }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql b/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql
new file mode 100644
index 00000000000..7486512c57c
--- /dev/null
+++ b/app/assets/javascripts/artifacts_settings/graphql/queries/get_keep_latest_artifact_project_setting.query.graphql
@@ -0,0 +1,7 @@
+query getKeepLatestArtifactProjectSetting($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ ciCdSettings {
+ keepLatestArtifact
+ }
+ }
+}
diff --git a/app/assets/javascripts/artifacts_settings/index.js b/app/assets/javascripts/artifacts_settings/index.js
new file mode 100644
index 00000000000..d99d2be81cf
--- /dev/null
+++ b/app/assets/javascripts/artifacts_settings/index.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import KeepLatestArtifactCheckbox from '~/artifacts_settings/keep_latest_artifact_checkbox.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default (containerId = 'js-artifacts-settings-app') => {
+ const containerEl = document.getElementById(containerId);
+
+ if (!containerEl) {
+ return false;
+ }
+
+ const { fullPath, helpPagePath } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ apolloProvider,
+ provide: {
+ fullPath,
+ helpPagePath,
+ },
+ render(createElement) {
+ return createElement(KeepLatestArtifactCheckbox);
+ },
+ });
+};
diff --git a/app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue b/app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue
new file mode 100644
index 00000000000..5684033f3af
--- /dev/null
+++ b/app/assets/javascripts/artifacts_settings/keep_latest_artifact_checkbox.vue
@@ -0,0 +1,99 @@
+<script>
+import { GlAlert, GlFormCheckbox, GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
+import GetKeepLatestArtifactProjectSetting from './graphql/queries/get_keep_latest_artifact_project_setting.query.graphql';
+import UpdateKeepLatestArtifactProjectSetting from './graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql';
+
+const FETCH_ERROR = __('There was a problem fetching the keep latest artifact setting.');
+const UPDATE_ERROR = __('There was a problem updating the keep latest artifact setting.');
+
+export default {
+ components: {
+ GlAlert,
+ GlFormCheckbox,
+ GlLink,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ helpPagePath: {
+ default: '',
+ },
+ },
+ apollo: {
+ keepLatestArtifact: {
+ query: GetKeepLatestArtifactProjectSetting,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ };
+ },
+ update(data) {
+ return data.project?.ciCdSettings?.keepLatestArtifact;
+ },
+ error() {
+ this.reportError(FETCH_ERROR);
+ },
+ },
+ },
+ data() {
+ return {
+ keepLatestArtifact: true,
+ errorMessage: '',
+ isAlertDismissed: false,
+ };
+ },
+ computed: {
+ shouldShowAlert() {
+ return this.errorMessage && !this.isAlertDismissed;
+ },
+ },
+ methods: {
+ reportError(error) {
+ this.errorMessage = error;
+ this.isAlertDismissed = false;
+ },
+ async updateSetting(checked) {
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: UpdateKeepLatestArtifactProjectSetting,
+ variables: {
+ fullPath: this.fullPath,
+ keepLatestArtifact: checked,
+ },
+ });
+
+ if (data.ciCdSettingsUpdate.errors.length) {
+ this.reportError(UPDATE_ERROR);
+ }
+ } catch (error) {
+ this.reportError(UPDATE_ERROR);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert
+ v-if="shouldShowAlert"
+ class="gl-mb-5"
+ variant="danger"
+ @dismiss="isAlertDismissed = true"
+ >{{ errorMessage }}</gl-alert
+ >
+ <gl-form-checkbox v-model="keepLatestArtifact" @change="updateSetting"
+ ><b class="gl-mr-3">{{ __('Keep artifacts from most recent successful jobs') }}</b>
+ <gl-link :href="helpPagePath">{{ __('More information') }}</gl-link></gl-form-checkbox
+ >
+ <p>
+ {{
+ __(
+ 'The latest artifacts created by jobs in the most recent successful pipeline will be stored.',
+ )
+ }}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 4bce0de7c12..c701ecd3040 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -2,8 +2,9 @@
import { GlModal } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
-import { visitUrl, stripFinalUrlSegment } from '~/lib/utils/url_utility';
-import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { getParameterByName } from '~/lib/utils/common_utils';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import boardsStore from '~/boards/stores/boards_store';
import { fullLabelId, fullBoardId } from '../boards_util';
@@ -216,9 +217,15 @@ export default {
variables: { input: this.mutationVariables },
});
- return this.board.id
- ? getIdFromGraphQLId(response.data.updateBoard.board.id)
- : getIdFromGraphQLId(response.data.createBoard.board.id);
+ if (!this.board.id) {
+ return response.data.createBoard.board.webPath;
+ }
+
+ const path = response.data.updateBoard.board.webPath;
+ const param = getParameterByName('group_by')
+ ? `?group_by=${getParameterByName('group_by')}`
+ : '';
+ return `${path}${param}`;
},
async submit() {
if (this.board.name.length === 0) return;
@@ -239,9 +246,7 @@ export default {
}
} else {
try {
- const path = await this.createOrUpdateBoard();
- const strippedUrl = stripFinalUrlSegment(window.location.href);
- const url = strippedUrl.includes('boards') ? `${path}` : `boards/${path}`;
+ const url = await this.createOrUpdateBoard();
visitUrl(url);
} catch {
Flash(this.$options.i18n.saveErrorMessage);
diff --git a/app/assets/javascripts/boards/graphql/board_create.mutation.graphql b/app/assets/javascripts/boards/graphql/board_create.mutation.graphql
index e26d67dcc0e..b3ea79d6443 100644
--- a/app/assets/javascripts/boards/graphql/board_create.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_create.mutation.graphql
@@ -2,6 +2,8 @@ mutation createBoard($input: CreateBoardInput!) {
createBoard(input: $input) {
board {
id
+ webPath
}
+ errors
}
}
diff --git a/app/assets/javascripts/boards/graphql/board_update.mutation.graphql b/app/assets/javascripts/boards/graphql/board_update.mutation.graphql
index 6b4ea2bef1a..3abe09079c7 100644
--- a/app/assets/javascripts/boards/graphql/board_update.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/board_update.mutation.graphql
@@ -2,8 +2,8 @@ mutation UpdateBoard($input: UpdateBoardInput!) {
updateBoard(input: $input) {
board {
id
- hideClosedList
- hideBacklogList
+ webPath
}
+ errors
}
}
diff --git a/app/assets/javascripts/ide/lib/languages/hcl.js b/app/assets/javascripts/ide/lib/languages/hcl.js
index d7a6491297b..bbb2ca66f33 100644
--- a/app/assets/javascripts/ide/lib/languages/hcl.js
+++ b/app/assets/javascripts/ide/lib/languages/hcl.js
@@ -144,7 +144,7 @@ const language = {
],
heredocBody: [
[
- /^([\w\-]+)$/,
+ /([\w\-]+)$/,
{
cases: {
'$1==$S2': [
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index d133e3655e3..eb97c458f88 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -19,6 +19,11 @@ export default {
required: false,
default: '',
},
+ classes: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
methods: {
openModal() {
@@ -29,7 +34,7 @@ export default {
</script>
<template>
- <gl-link @click="openModal">
+ <gl-link :class="classes" @click="openModal">
<div v-if="icon" class="nav-icon-container">
<gl-icon :size="16" :name="icon" />
</div>
diff --git a/app/assets/javascripts/merge_request/components/status_box.vue b/app/assets/javascripts/merge_request/components/status_box.vue
index c29f7b86df9..fd99802caff 100644
--- a/app/assets/javascripts/merge_request/components/status_box.vue
+++ b/app/assets/javascripts/merge_request/components/status_box.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import mrEventHub from '../eventhub';
@@ -18,29 +18,16 @@ const STATUS = {
export default {
components: {
GlIcon,
- GlSprintf,
- GlLink,
},
props: {
initialState: {
type: String,
required: true,
},
- initialIsReverted: {
- type: Boolean,
- required: true,
- },
- initialRevertedPath: {
- type: String,
- required: false,
- default: null,
- },
},
data() {
return {
state: this.initialState,
- isReverted: this.initialIsReverted,
- revertedPath: this.initialRevertedPath,
};
},
computed: {
@@ -61,10 +48,8 @@ export default {
mrEventHub.$off('mr.state.updated', this.updateState);
},
methods: {
- updateState({ state, reverted, revertedPath }) {
+ updateState({ state }) {
this.state = state;
- this.reverted = reverted;
- this.revertedPath = revertedPath;
},
},
};
@@ -78,17 +63,7 @@ export default {
data-testid="status-icon"
/>
<span class="gl-display-none gl-display-sm-block">
- <gl-sprintf v-if="isReverted" :message="__('Merged (%{linkStart}reverted%{linkEnd})')">
- <template #link="{ content }">
- <gl-link
- :href="revertedPath"
- class="gl-reset-color! gl-text-decoration-underline"
- data-testid="reverted-link"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- <template v-else>{{ statusHumanName }}</template>
+ {{ statusHumanName }}
</span>
</div>
</template>
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index dbc65f67a98..5346e3720e8 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -4,6 +4,8 @@ import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import { initGroupMembersApp } from '~/groups/members';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { memberRequestFormatter, groupLinkRequestFormatter } from '~/groups/members/utils';
import { s__ } from '~/locale';
@@ -64,5 +66,7 @@ groupsSelect();
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
+initInviteMembersModal();
+initInviteMembersTrigger();
new UsersSelect(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 7657cea5bcd..1a0c5860991 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import ZenMode from '~/zen_mode';
import initIssuableSidebar from '~/init_issuable_sidebar';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
-import { handleLocationHash, parseBoolean } from '~/lib/utils/common_utils';
+import { handleLocationHash } from '~/lib/utils/common_utils';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initSourcegraph from '~/sourcegraph';
import loadAwardsHandler from '~/awards_handler';
@@ -29,8 +29,6 @@ export default function () {
return h(StatusBox, {
props: {
initialState: el.dataset.state,
- initialIsReverted: parseBoolean(el.dataset.isReverted),
- initialRevertedPath: el.dataset.revertedPath,
},
});
},
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 664f0a2dc93..3e0a48ee6a2 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -4,6 +4,8 @@ import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
@@ -24,6 +26,8 @@ document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
+ initInviteMembersModal();
+ initInviteMembersTrigger();
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 35c0696f0f1..1321155b7ec 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -5,6 +5,7 @@ import initVariableList from '~/ci_variable_list';
import initDeployFreeze from '~/deploy_freeze';
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
+import initArtifactsSettings from '~/artifacts_settings';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
@@ -33,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => {
initDeployFreeze();
initSettingsPipelinesTriggers();
+ initArtifactsSettings();
if (gon?.features?.vueifySharedRunnersToggle) {
initSharedRunnersToggle();
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index da4dc777c3e..a6bbab47a06 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -158,8 +158,6 @@ export default class MergeRequestStore {
mrEventHub.$emit('mr.state.updated', {
state: this.mergeRequestState,
- reverted: data.reverted,
- reverted_path: data.revertedPath,
});
}
diff --git a/app/controllers/jira_connect/app_descriptor_controller.rb b/app/controllers/jira_connect/app_descriptor_controller.rb
index 4cee6e5b479..137f830e40b 100644
--- a/app/controllers/jira_connect/app_descriptor_controller.rb
+++ b/app/controllers/jira_connect/app_descriptor_controller.rb
@@ -66,6 +66,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
modules.merge!(build_information_module)
modules.merge!(deployment_information_module)
+ modules.merge!(feature_flag_module)
modules
end
@@ -85,6 +86,19 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
}
end
+ # see: https://developer.atlassian.com/cloud/jira/software/modules/feature-flag/
+ def feature_flag_module
+ {
+ jiraFeatureFlagInfoProvider: common_module_properties.merge(
+ actions: {}, # TODO: create, link and list feature flags https://gitlab.com/gitlab-org/gitlab/-/issues/297386
+ name: {
+ value: 'GitLab Feature Flags'
+ },
+ key: 'gitlab-feature-flags'
+ )
+ }
+ end
+
# See: https://developer.atlassian.com/cloud/jira/software/modules/build/
def build_information_module
{
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index c2089a0fca3..1ef1e12bb02 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -18,9 +18,19 @@ class MetricsController < ActionController::Base
render plain: response, content_type: 'text/plain; version=0.0.4'
end
+ def system
+ render json: system_metrics
+ end
+
private
def metrics_service
@metrics_service ||= MetricsService.new
end
+
+ def system_metrics
+ Gitlab::Metrics::System.summary.merge(
+ worker_id: Prometheus::PidProvider.worker_id
+ )
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 2a4e382e27a..59f2a1539ef 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -35,7 +35,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:approvals_commented_by, @project, default_enabled: true)
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, default_enabled: true)
push_frontend_feature_flag(:merge_request_widget_graphql, @project)
- push_frontend_feature_flag(:unified_diff_components, @project)
+ push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
diff --git a/app/graphql/resolvers/ci/config_resolver.rb b/app/graphql/resolvers/ci/config_resolver.rb
index bea91b5540f..72d3ae30d73 100644
--- a/app/graphql/resolvers/ci/config_resolver.rb
+++ b/app/graphql/resolvers/ci/config_resolver.rb
@@ -18,38 +18,49 @@ module Resolvers
required: true,
description: "Contents of '.gitlab-ci.yml'."
- def resolve(project_path:, content:)
- project = authorized_find!(project_path: project_path)
+ argument :dry_run, GraphQL::BOOLEAN_TYPE,
+ required: false,
+ description: 'Run pipeline creation simulation, or only do static check.'
- result = ::Gitlab::Ci::YamlProcessor.new(content, project: project,
- user: current_user,
- sha: project.repository.commit.sha).execute
+ def resolve(project_path:, content:, dry_run: false)
+ project = authorized_find!(project_path: project_path)
- response = if result.errors.empty?
- {
- status: :valid,
- errors: [],
- stages: make_stages(result.jobs)
- }
- else
- {
- status: :invalid,
- errors: result.errors
- }
- end
+ result = ::Gitlab::Ci::Lint
+ .new(project: project, current_user: context[:current_user])
+ .validate(content, dry_run: dry_run)
- response.merge(merged_yaml: result.merged_yaml)
+ if result.errors.empty?
+ {
+ status: :valid,
+ errors: [],
+ stages: make_stages(result.jobs)
+ }
+ else
+ {
+ status: :invalid,
+ errors: result.errors
+ }
+ end
end
private
def make_jobs(config_jobs)
- config_jobs.map do |job_name, job|
+ config_jobs.map do |job|
{
- name: job_name,
+ name: job[:name],
stage: job[:stage],
- group_name: CommitStatus.new(name: job_name).group_name,
- needs: job.dig(:needs, :job) || []
+ group_name: CommitStatus.new(name: job[:name]).group_name,
+ needs: job.dig(:needs) || [],
+ allow_failure: job[:allow_failure],
+ before_script: job[:before_script],
+ script: job[:script],
+ after_script: job[:after_script],
+ only: job[:only],
+ except: job[:except],
+ when: job[:when],
+ tags: job[:tag_list],
+ environment: job[:environment]
}
end
end
diff --git a/app/graphql/types/ci/config/job_restriction_type.rb b/app/graphql/types/ci/config/job_restriction_type.rb
new file mode 100644
index 00000000000..294e3c94571
--- /dev/null
+++ b/app/graphql/types/ci/config/job_restriction_type.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ module Config
+ class JobRestrictionType < BaseObject
+ graphql_name 'CiConfigJobRestriction'
+
+ field :refs, [GraphQL::STRING_TYPE], null: true,
+ description: 'The Git refs the job restriction applies to.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/config/job_type.rb b/app/graphql/types/ci/config/job_type.rb
index 855875ba300..65fdc4c2615 100644
--- a/app/graphql/types/ci/config/job_type.rb
+++ b/app/graphql/types/ci/config/job_type.rb
@@ -8,13 +8,36 @@ module Types
graphql_name 'CiConfigJob'
field :name, GraphQL::STRING_TYPE, null: true,
- description: 'Name of the job'
+ description: 'Name of the job.'
field :group_name, GraphQL::STRING_TYPE, null: true,
- description: 'Name of the job group'
+ description: 'Name of the job group.'
field :stage, GraphQL::STRING_TYPE, null: true,
- description: 'Name of the job stage'
+ description: 'Name of the job stage.'
field :needs, Types::Ci::Config::NeedType.connection_type, null: true,
- description: 'Builds that must complete before the jobs run'
+ description: 'Builds that must complete before the jobs run.'
+ field :allow_failure, GraphQL::BOOLEAN_TYPE, null: true,
+ description: 'Allow job to fail.'
+ field :before_script, [GraphQL::STRING_TYPE], null: true,
+ description: 'Override a set of commands that are executed before the job.'
+ field :script, [GraphQL::STRING_TYPE], null: true,
+ description: 'Shell script that is executed by a runner.'
+ field :after_script, [GraphQL::STRING_TYPE], null: true,
+ description: 'Override a set of commands that are executed after the job.'
+ field :when, GraphQL::STRING_TYPE, null: true,
+ description: 'When to run the job.',
+ resolver_method: :restrict_when_to_run_jobs
+ field :environment, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of an environment to which the job deploys.'
+ field :except, Types::Ci::Config::JobRestrictionType, null: true,
+ description: 'Limit when jobs are not created.'
+ field :only, Types::Ci::Config::JobRestrictionType, null: true,
+ description: 'Jobs are created when these conditions do not apply.'
+ field :tags, [GraphQL::STRING_TYPE], null: true,
+ description: 'List of tags that are used to select a runner.'
+
+ def restrict_when_to_run_jobs
+ object[:when]
+ end
end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 6a5d69f2e73..56f33ed1e1d 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -428,10 +428,6 @@ class Commit
end
def has_been_reverted?(current_user, notes_association = nil)
- reverting_commit(current_user, notes_association).present?
- end
-
- def reverting_commit(current_user, notes_association = nil)
ext = Gitlab::ReferenceExtractor.new(project, current_user)
notes_association ||= notes_with_associations
@@ -439,7 +435,7 @@ class Commit
note.all_references(current_user, extractor: ext)
end
- ext.commits.find { |commit_ref| commit_ref.reverts_commit?(self, current_user) }
+ ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) }
end
def change_type_title(user)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 3918498f694..64b8223a1f0 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1593,26 +1593,6 @@ class MergeRequest < ApplicationRecord
!merge_commit.has_been_reverted?(current_user, notes_association)
end
- def reverted_by_merge_request?(current_user)
- reverting_merge_request(current_user).present?
- end
-
- def reverting_merge_request(current_user)
- return unless merge_commit
- return unless merged_at
-
- reverting_commit = merge_commit.reverting_commit(current_user, notes_with_associations)
-
- if reverting_commit
- MergeRequestsFinder.new(
- current_user,
- project_id: project.id,
- commit_sha: reverting_commit.sha,
- state: 'merged'
- ).execute.first
- end
- end
-
def merged_at
strong_memoize(:merged_at) do
next unless merged?
diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb
index df7c7d2defa..1db4ec37d4a 100644
--- a/app/serializers/merge_request_poll_cached_widget_entity.rb
+++ b/app/serializers/merge_request_poll_cached_widget_entity.rb
@@ -114,14 +114,6 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
end
end
- expose :reverted do |merge_request|
- merge_request.reverted_by_merge_request?(current_user)
- end
-
- expose :reverted_path, if: -> (mr) { mr.reverted_by_merge_request?(current_user) } do |merge_request|
- merge_request_path(merge_request.reverting_merge_request(current_user))
- end
-
private
delegate :current_user, to: :request
diff --git a/app/services/feature_flags/base_service.rb b/app/services/feature_flags/base_service.rb
index 9b27df90992..c11c465252e 100644
--- a/app/services/feature_flags/base_service.rb
+++ b/app/services/feature_flags/base_service.rb
@@ -6,6 +6,11 @@ module FeatureFlags
AUDITABLE_ATTRIBUTES = %w(name description active).freeze
+ def success(**args)
+ sync_to_jira(args[:feature_flag])
+ super
+ end
+
protected
def audit_event(feature_flag)
@@ -34,6 +39,16 @@ module FeatureFlags
audit_event.security_event
end
+ def sync_to_jira(feature_flag)
+ return unless feature_flag.present?
+ return unless Feature.enabled?(:jira_sync_feature_flags, feature_flag.project)
+
+ seq_id = ::Atlassian::JiraConnect::Client.generate_update_sequence_id
+ feature_flag.run_after_commit do
+ ::JiraConnect::SyncFeatureFlagsWorker.perform_async(feature_flag.id, seq_id)
+ end
+ end
+
def created_scope_message(scope)
"Created rule <strong>#{scope.environment_scope}</strong> "\
"and set it as <strong>#{scope.active ? "active" : "inactive"}</strong> "\
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index e3fd9d4bb17..25b7fa2a718 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -6,20 +6,28 @@
.js-remove-member-modal
.project-members-page.gl-mt-3
- %h4
- = _('Group members')
- %hr
- - if can_manage_members
- %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
- %li.nav-tab{ role: 'presentation' }
- %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _('Invite member')
+ .gl-display-flex.gl-flex-wrap
+ - if can_manage_members
+ .gl-w-half.gl-xs-w-full
+ %h4
+ = _('Group members')
+ - if invite_members_allowed?(@group)
+ .gl-w-half.gl-xs-w-full
+ .gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2.gl-mb-3
+ .js-invite-members-trigger.gl-px-2.gl-sm-w-auto.gl-w-full.gl-mb-4{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite members') } }
+ = render_if_exists 'groups/invite_members_modal', group: @group
+ - if can_manage_members && !invite_members_allowed?(@group)
+ %hr.gl-mt-4
+ %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
- %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _('Invite group')
- .tab-content.gitlab-tab-content
- .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
- = render_invite_member_for_group(@group, @group_member.access_level)
- .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
- = render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access'
+ %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _('Invite member')
+ %li.nav-tab{ role: 'presentation' }
+ %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab', qa_selector: 'invite_group_tab' }, role: 'tab' }= _('Invite group')
+ .tab-content.gitlab-tab-content
+ .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
+ = render_invite_member_for_group(@group, @group_member.access_level)
+ .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' }
+ = render 'shared/members/invite_group', submit_url: group_group_links_path(@group), access_levels: GroupMember.access_level_roles, default_access_level: @group_member.access_level, group_link_field: 'shared_with_group_id', group_access_field: 'shared_group_access'
= render_if_exists 'groups/group_members/ldap_sync'
diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml
index 178a9d3f8b4..9f850842f58 100644
--- a/app/views/profiles/two_factor_auths/_codes.html.haml
+++ b/app/views/profiles/two_factor_auths/_codes.html.haml
@@ -1,18 +1,3 @@
- show_success_alert = local_assigns.fetch(:show_success_alert, nil)
-- if Feature.enabled?(:vue_2fa_recovery_codes, current_user, default_enabled: true)
- .js-2fa-recovery-codes{ data: { codes: @codes.to_json, profile_account_path: profile_account_path(two_factor_auth_enabled_successfully: show_success_alert) } }
-- else
- %p.slead
- - lose_2fa_message = _('Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account.') % { b_start:'<b>', b_end:'</b>' }
- = lose_2fa_message.html_safe
-
- .codes.card{ data: { qa_selector: 'codes_content' } }
- %ul
- - @codes.each do |code|
- %li
- %span.monospace{ data: { qa_selector: 'code_content' } }= code
-
- .d-flex
- = link_to _('Proceed'), profile_account_path, class: 'gl-button btn btn-success gl-mr-3', data: { qa_selector: 'proceed_button' }
- = link_to _('Download codes'), "data:text/plain;charset=utf-8,#{CGI.escape(@codes.join("\n"))}", download: "gitlab-recovery-codes.txt", class: 'gl-button btn btn-default'
+.js-2fa-recovery-codes{ data: { codes: @codes.to_json, profile_account_path: profile_account_path(two_factor_auth_enabled_successfully: show_success_alert) } }
diff --git a/app/views/profiles/two_factor_auths/create.html.haml b/app/views/profiles/two_factor_auths/create.html.haml
index be4800024cf..606dda5ed55 100644
--- a/app/views/profiles/two_factor_auths/create.html.haml
+++ b/app/views/profiles/two_factor_auths/create.html.haml
@@ -1,8 +1,4 @@
- page_title _('Two-factor Authentication'), _('Account')
- add_page_specific_style 'page_bundles/profile_two_factor_auth'
-- unless Feature.enabled?(:vue_2fa_recovery_codes, current_user, default_enabled: true)
- .gl-alert.gl-alert-success.gl-mb-5
- = _('Congratulations! You have enabled Two-factor Authentication!')
-
= render 'codes', show_success_alert: true
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 2d7218740ef..4711143c900 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -40,12 +40,11 @@
.issuable-meta
%ul.controls.d-flex.align-items-end
- %li.issuable-status.d-none.d-sm-inline-block
- - if merge_request.reverted_by_merge_request?(current_user)
- = _('MERGED (REVERTED)')
- - elsif merge_request.merged?
+ - if merge_request.merged?
+ %li.issuable-status.d-none.d-sm-inline-block
= _('MERGED')
- - elsif merge_request.closed?
+ - elsif merge_request.closed?
+ %li.issuable-status.d-none.d-sm-inline-block
= sprite_icon('cancel', css_class: 'gl-vertical-align-text-bottom')
= _('CLOSED')
= render 'shared/merge_request_pipeline_status', merge_request: merge_request
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index c423df49b2b..6a42f33db7d 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -3,8 +3,6 @@
- can_reopen_merge_request = can?(current_user, :reopen_merge_request, @merge_request)
- state_human_name, state_icon_name = state_name_with_icon(@merge_request)
- are_close_and_open_buttons_hidden = merge_request_button_hidden?(@merge_request, true) && merge_request_button_hidden?(@merge_request, false)
-- is_reverted = @merge_request.reverted_by_merge_request?(current_user)
-- reverted_mr_path = is_reverted ? merge_request_path(@merge_request.reverting_merge_request(current_user)) : nil
- if @merge_request.closed_or_merged_without_fork?
.gl-alert.gl-alert-danger.gl-mb-5
@@ -14,13 +12,10 @@
.detail-page-header.border-bottom-0.pt-0.pb-0
.detail-page-header-body
- .issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(@merge_request), data: { state: @merge_request.state, is_reverted: is_reverted.to_s, reverted_path: reverted_mr_path } }
+ .issuable-status-box.status-box.js-mr-status-box{ class: status_box_class(@merge_request), data: { state: @merge_request.state } }
= sprite_icon(state_icon_name, css_class: 'gl-display-block gl-display-sm-none!')
%span.gl-display-none.gl-display-sm-block
- - if @merge_request.reverted_by_merge_request?(current_user)
- = _('Merged (%{reverted})').html_safe % { reverted: link_to(s_('MergeRequest|reverted'), reverted_mr_path, class: 'gl-reset-color! gl-text-decoration-underline') }
- - else
- = state_human_name
+ = state_human_name
.issuable-meta
#js-issuable-header-warnings
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 0f5f169f548..cf39ac4dd56 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -4,16 +4,34 @@
.js-remove-member-modal
.row.gl-mt-3
.col-lg-12
- - if project_can_be_shared?
- %h4
- = _("Project members")
- - if can_manage_project_members?(@project)
- %p= share_project_description(@project)
- - else
- %p
- = html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
+ - if invite_members_allowed?(group)
+ .row
+ .col-md-12.col-lg-6.gl-display-flex
+ .gl-flex-direction-column.gl-flex-wrap.align-items-baseline
+ %h4
+ = _("Project members")
+ .gl-justify-content-bottom.gl-display-flex.align-items-center
+ - if can_manage_project_members?(@project)
+ %p= share_project_description(@project)
+ - else
+ %p
+ = html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
+ .col-md-12.col-lg-6
+ .gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2.gl-mb-3
+ .js-invite-members-trigger.gl-px-2.gl-sm-w-auto.gl-w-full.gl-mb-4{ data: { classes: 'btn btn-success gl-button gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite members') } }
+ = render_if_exists 'projects/invite_members_modal', project: @project
- - if can_manage_project_members?(@project) && project_can_be_shared?
+ - else
+ - if project_can_be_shared?
+ %h4
+ = _("Project members")
+ - if can_manage_project_members?(@project)
+ %p= share_project_description(@project)
+ - else
+ %p
+ = html_escape(_("Members can be added by project %{i_open}Maintainers%{i_close} or %{i_open}Owners%{i_close}")) % { i_open: '<i>'.html_safe, i_close: '</i>'.html_safe }
+
+ - if !invite_members_allowed?(group) && can_manage_project_members?(@project) && project_can_be_shared?
- if !membership_locked? && @project.allowed_to_share_with_group?
%ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
%li.nav-tab{ role: 'presentation' }
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 7bc4818b625..55b6cf372fb 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -45,6 +45,17 @@
.settings-content
= render 'projects/runners/index'
+%section.settings.no-animate#js-artifacts-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _("Artifacts")
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _("A job artifact is an archive of files and directories saved by a job when it finishes.")
+ .settings-content
+ #js-artifacts-settings-app{ data: { full_path: @project.full_path, help_page_path: help_page_path('ci/pipelines/job_artifacts', anchor: 'keep-artifacts-from-most-recent-successful-jobs') } }
+
%section.qa-variables-settings.settings.no-animate#js-cicd-variables-settings{ class: ('expanded' if expanded), data: { qa_selector: 'variables_settings_content' } }
.settings-header
= render 'ci/variables/header', expanded: expanded
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 05a41fceafe..9ac10aa2d7c 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -907,6 +907,14 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: jira_connect:jira_connect_sync_feature_flags
+ :feature_category: :integrations
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: jira_connect:jira_connect_sync_merge_request
:feature_category: :integrations
:has_external_dependencies: true
diff --git a/app/workers/jira_connect/sync_feature_flags_worker.rb b/app/workers/jira_connect/sync_feature_flags_worker.rb
new file mode 100644
index 00000000000..7e98d0eada7
--- /dev/null
+++ b/app/workers/jira_connect/sync_feature_flags_worker.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module JiraConnect
+ class SyncFeatureFlagsWorker
+ include ApplicationWorker
+
+ idempotent!
+ worker_has_external_dependencies!
+
+ queue_namespace :jira_connect
+ feature_category :integrations
+
+ def perform(feature_flag_id, sequence_id)
+ feature_flag = ::Operations::FeatureFlag.find_by_id(feature_flag_id)
+
+ return unless feature_flag
+ return unless Feature.enabled?(:jira_sync_feature_flags, feature_flag.project)
+
+ ::JiraConnect::SyncService
+ .new(feature_flag.project)
+ .execute(feature_flags: [feature_flag], update_sequence_id: sequence_id)
+ end
+ end
+end
diff --git a/changelogs/unreleased/282299-grouping-swimlanes-choice-should-persist-after-board-has-been-edit.yml b/changelogs/unreleased/282299-grouping-swimlanes-choice-should-persist-after-board-has-been-edit.yml
new file mode 100644
index 00000000000..0aeedc2530f
--- /dev/null
+++ b/changelogs/unreleased/282299-grouping-swimlanes-choice-should-persist-after-board-has-been-edit.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Grouping/swimlanes choice should persist after board has been edited
+merge_request: 51317
+author:
+type: changed
diff --git a/changelogs/unreleased/35824-Indicate-Reverted-MRs-in-MR-Header.yml b/changelogs/unreleased/35824-Indicate-Reverted-MRs-in-MR-Header.yml
deleted file mode 100644
index fe8a531c463..00000000000
--- a/changelogs/unreleased/35824-Indicate-Reverted-MRs-in-MR-Header.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Indicate reverted status of a merged merge request
-merge_request: 45471
-author: Kev @KevSlashNull
-type: added
diff --git a/changelogs/unreleased/lm-add-new-jobs-fields-graphql.yml b/changelogs/unreleased/lm-add-new-jobs-fields-graphql.yml
new file mode 100644
index 00000000000..20220a1d4be
--- /dev/null
+++ b/changelogs/unreleased/lm-add-new-jobs-fields-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Updates graphql gitlab-ci.yml linter implementation
+merge_request: 50664
+author:
+type: changed
diff --git a/changelogs/unreleased/pb-keep-latest-artifact-setting-ui.yml b/changelogs/unreleased/pb-keep-latest-artifact-setting-ui.yml
new file mode 100644
index 00000000000..15f2d403a47
--- /dev/null
+++ b/changelogs/unreleased/pb-keep-latest-artifact-setting-ui.yml
@@ -0,0 +1,5 @@
+---
+title: UI to opt out of keeping the artifacts from the last job at project level.
+merge_request: 49500
+author:
+type: added
diff --git a/config/feature_flags/development/ci_pipeline_open_merge_requests.yml b/config/feature_flags/development/ci_pipeline_open_merge_requests.yml
index d50e49b6aa1..7e2ae1edd34 100644
--- a/config/feature_flags/development/ci_pipeline_open_merge_requests.yml
+++ b/config/feature_flags/development/ci_pipeline_open_merge_requests.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292727
milestone: '13.7'
group: group::memory
type: development
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/vue_2fa_recovery_codes.yml b/config/feature_flags/development/jira_sync_feature_flags.yml
index 7995b00f9ab..c5925766919 100644
--- a/config/feature_flags/development/vue_2fa_recovery_codes.yml
+++ b/config/feature_flags/development/jira_sync_feature_flags.yml
@@ -1,8 +1,8 @@
---
-name: vue_2fa_recovery_codes
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49078
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290113
-milestone: '13.7'
+name: jira_sync_feature_flags
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50390
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/296990
+milestone: '13.8'
type: development
-group: group::access
-default_enabled: true
+group: group::ecosystem
+default_enabled: false
diff --git a/config/feature_flags/development/unified_diff_components.yml b/config/feature_flags/development/unified_diff_components.yml
index 68a1cd8996f..0176d8c55ba 100644
--- a/config/feature_flags/development/unified_diff_components.yml
+++ b/config/feature_flags/development/unified_diff_components.yml
@@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44974
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/268039
type: development
group: group::code review
-default_enabled: false
+default_enabled: true
diff --git a/config/routes.rb b/config/routes.rb
index 3ba78ead0a8..16e833ede71 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -92,7 +92,10 @@ Rails.application.routes.draw do
# '/-/health' implemented by BasicHealthCheck middleware
get 'liveness' => 'health#liveness'
get 'readiness' => 'health#readiness'
- resources :metrics, only: [:index]
+ controller :metrics do
+ get 'metrics', action: :index
+ get 'metrics/system', action: :system
+ end
mount Peek::Railtie => '/peek', as: 'peek_routes'
get 'runner_setup/platforms' => 'runner_setup#platforms'
diff --git a/db/structure.sql b/db/structure.sql
index 6ee47d3b30c..3d0da282548 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -21993,6 +21993,8 @@ CREATE UNIQUE INDEX index_labels_on_group_id_and_project_id_and_title ON labels
CREATE UNIQUE INDEX index_labels_on_group_id_and_title_unique ON labels USING btree (group_id, title) WHERE (project_id IS NULL);
+CREATE INDEX index_labels_on_group_id_and_title_with_null_project_id ON labels USING btree (group_id, title) WHERE (project_id IS NULL);
+
CREATE INDEX index_labels_on_project_id ON labels USING btree (project_id);
CREATE UNIQUE INDEX index_labels_on_project_id_and_title_unique ON labels USING btree (project_id, title) WHERE (group_id IS NULL);
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 584f2061666..213d963bf16 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2496,17 +2496,42 @@ type CiConfigGroupEdge {
type CiConfigJob {
"""
- Name of the job group
+ Override a set of commands that are executed after the job.
+ """
+ afterScript: [String!]
+
+ """
+ Allow job to fail.
+ """
+ allowFailure: Boolean
+
+ """
+ Override a set of commands that are executed before the job.
+ """
+ beforeScript: [String!]
+
+ """
+ Name of an environment to which the job deploys.
+ """
+ environment: String
+
+ """
+ Limit when jobs are not created.
+ """
+ except: CiConfigJobRestriction
+
+ """
+ Name of the job group.
"""
groupName: String
"""
- Name of the job
+ Name of the job.
"""
name: String
"""
- Builds that must complete before the jobs run
+ Builds that must complete before the jobs run.
"""
needs(
"""
@@ -2531,9 +2556,29 @@ type CiConfigJob {
): CiConfigNeedConnection
"""
- Name of the job stage
+ Jobs are created when these conditions do not apply.
+ """
+ only: CiConfigJobRestriction
+
+ """
+ Shell script that is executed by a runner.
+ """
+ script: [String!]
+
+ """
+ Name of the job stage.
"""
stage: String
+
+ """
+ List of tags that are used to select a runner.
+ """
+ tags: [String!]
+
+ """
+ When to run the job.
+ """
+ when: String
}
"""
@@ -2571,6 +2616,13 @@ type CiConfigJobEdge {
node: CiConfigJob
}
+type CiConfigJobRestriction {
+ """
+ The Git refs the job restriction applies to.
+ """
+ refs: [String!]
+}
+
type CiConfigNeed {
"""
Name of the need
@@ -19640,6 +19692,11 @@ type Query {
content: String!
"""
+ Run pipeline creation simulation, or only do static check.
+ """
+ dryRun: Boolean
+
+ """
The project of the CI config.
"""
projectPath: ID!
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index ea5cbb888a3..23b4ebd9553 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -6712,8 +6712,94 @@
"description": null,
"fields": [
{
+ "name": "afterScript",
+ "description": "Override a set of commands that are executed after the job.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "allowFailure",
+ "description": "Allow job to fail.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "beforeScript",
+ "description": "Override a set of commands that are executed before the job.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "environment",
+ "description": "Name of an environment to which the job deploys.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "except",
+ "description": "Limit when jobs are not created.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CiConfigJobRestriction",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "groupName",
- "description": "Name of the job group",
+ "description": "Name of the job group.",
"args": [
],
@@ -6727,7 +6813,7 @@
},
{
"name": "name",
- "description": "Name of the job",
+ "description": "Name of the job.",
"args": [
],
@@ -6741,7 +6827,7 @@
},
{
"name": "needs",
- "description": "Builds that must complete before the jobs run",
+ "description": "Builds that must complete before the jobs run.",
"args": [
{
"name": "after",
@@ -6793,8 +6879,80 @@
"deprecationReason": null
},
{
+ "name": "only",
+ "description": "Jobs are created when these conditions do not apply.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CiConfigJobRestriction",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "script",
+ "description": "Shell script that is executed by a runner.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "stage",
- "description": "Name of the job stage",
+ "description": "Name of the job stage.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "tags",
+ "description": "List of tags that are used to select a runner.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "when",
+ "description": "When to run the job.",
"args": [
],
@@ -6928,6 +7086,41 @@
},
{
"kind": "OBJECT",
+ "name": "CiConfigJobRestriction",
+ "description": null,
+ "fields": [
+ {
+ "name": "refs",
+ "description": "The Git refs the job restriction applies to.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "CiConfigNeed",
"description": null,
"fields": [
@@ -57308,6 +57501,16 @@
}
},
"defaultValue": null
+ },
+ {
+ "name": "dryRun",
+ "description": "Run pipeline creation simulation, or only do static check.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ },
+ "defaultValue": null
}
],
"type": {
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9a6a515b764..4930b136894 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -413,10 +413,25 @@ Autogenerated return type of CiCdSettingsUpdate.
| Field | Type | Description |
| ----- | ---- | ----------- |
-| `groupName` | String | Name of the job group |
-| `name` | String | Name of the job |
-| `needs` | CiConfigNeedConnection | Builds that must complete before the jobs run |
-| `stage` | String | Name of the job stage |
+| `afterScript` | String! => Array | Override a set of commands that are executed after the job. |
+| `allowFailure` | Boolean | Allow job to fail. |
+| `beforeScript` | String! => Array | Override a set of commands that are executed before the job. |
+| `environment` | String | Name of an environment to which the job deploys. |
+| `except` | CiConfigJobRestriction | Limit when jobs are not created. |
+| `groupName` | String | Name of the job group. |
+| `name` | String | Name of the job. |
+| `needs` | CiConfigNeedConnection | Builds that must complete before the jobs run. |
+| `only` | CiConfigJobRestriction | Jobs are created when these conditions do not apply. |
+| `script` | String! => Array | Shell script that is executed by a runner. |
+| `stage` | String | Name of the job stage. |
+| `tags` | String! => Array | List of tags that are used to select a runner. |
+| `when` | String | When to run the job. |
+
+### CiConfigJobRestriction
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `refs` | String! => Array | The Git refs the job restriction applies to. |
### CiConfigNeed
diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md
index 8ea94ad72bb..27cbde9285f 100644
--- a/doc/ci/pipelines/job_artifacts.md
+++ b/doc/ci/pipelines/job_artifacts.md
@@ -464,6 +464,23 @@ To retrieve a job artifact from a different project, you might need to use a
private token to [authenticate and download](../../api/job_artifacts.md#get-job-artifacts)
the artifact.
+## Keep artifacts from most recent successful jobs
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16267) in GitLab 13.0.
+> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/229936) in GitLab 13.4.
+> - [Made optional with a CI/CD setting](https://gitlab.com/gitlab-org/gitlab/-/issues/241026) in GitLab 13.8.
+
+By default, the latest artifacts from the most recent successful jobs are never deleted.
+If a job is configured with [`expire_in`](../yaml/README.md#artifactsexpire_in),
+its artifacts only expire if a more recent artifact exists.
+
+Keeping the latest artifacts can use a large amount of storage space in projects
+with a lot of jobs or large artifacts. If the latest artifacts are not needed in
+a project, you can disable this behavior to save space:
+
+1. Navigate to **Settings > CI/CD > Artifacts**.
+1. Uncheck **Keep artifacts from most recent successful jobs**.
+
## Troubleshooting
### Error message `No files to upload`
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 6453ffda28c..779b0c4732c 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -66,7 +66,7 @@ Kubernetes-specific environment variables are detailed in the
| `CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME` | 12.3 | all | The target branch name of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA` | 12.3 | all | The HEAD SHA of the target branch of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_HAS_OPEN_REQUIREMENTS` | 13.1 | all | Included with the value `true` only if the pipeline's project has any open [requirements](../../user/project/requirements/index.md). Not included if there are no open requirements for the pipeline's project. |
-| `CI_OPEN_MERGE_REQUESTS` | 13.7 | all | Contains a comma-delimited list of up to 4 Merge Requests from the current source project and branch in the form `gitlab-org/gitlab!333,gitlab-org/gitlab-foss!11`. |
+| `CI_OPEN_MERGE_REQUESTS` | 13.8 | all | Available in branch and merge request pipelines. Contains a comma-separated list of up to four merge requests that use the current branch and project as the merge request source. For example `gitlab-org/gitlab!333,gitlab-org/gitlab-foss!11`. |
| `CI_JOB_ID` | 9.0 | all | The unique ID of the current job that GitLab CI/CD uses internally. |
| `CI_JOB_IMAGE` | 12.9 | 12.9 | The name of the image running the CI job. |
| `CI_JOB_MANUAL` | 8.12 | all | The flag to indicate that job was manually started. |
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 8c881460606..cfc69d473de 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -3363,7 +3363,7 @@ job:
The latest artifacts for refs are locked against deletion, and kept regardless of
the expiry time. [Introduced in](https://gitlab.com/gitlab-org/gitlab/-/issues/16267)
GitLab 13.0 behind a disabled feature flag, and [made the default behavior](https://gitlab.com/gitlab-org/gitlab/-/issues/229936)
-in GitLab 13.4.
+in GitLab 13.4. In [GitLab 13.8 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/241026), you can [disable this behavior in the CI/CD settings](../pipelines/job_artifacts.md#keep-artifacts-from-most-recent-successful-jobs).
#### `artifacts:reports`
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index fb8b83465fd..0354e703357 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -422,24 +422,29 @@ We are using a custom mapping between source file to test files, maintained in t
### PostgreSQL versions testing
+Even though [Omnibus defaults to PG12 for new installs and upgrades](https://docs.gitlab.com/omnibus/package-information/postgresql_versions.md),
+our test suite is currently running against PG11, since GitLab.com still runs on PG11.
+
+We do run our test suite against PG12 on nightly scheduled pipelines as well as upon specific
+database library changes in MRs and `master` pipelines (with the `rspec db-library-code pg12` job).
+
#### Current versions testing
| Where? | PostgreSQL version |
-| ------ | ------ |
-| MRs | 11 |
-| `master` (non-scheduled pipelines) | 11 |
-| 2-hourly scheduled pipelines | 11 |
+| ------ | ------------------ |
+| MRs | 11, 12 for DB library changes |
+| `master` (non-scheduled pipelines) | 11, 12 for DB library changes |
+| 2-hourly scheduled pipelines | 11, 12 for DB library changes |
| `nightly` scheduled pipelines | 11, 12 |
#### Long-term plan
We follow the [PostgreSQL versions shipped with Omnibus GitLab](https://docs.gitlab.com/omnibus/package-information/postgresql_versions.html):
-| PostgreSQL version | 13.0 (May 2020) | 13.1 (June 2020) | 13.2 (July 2020) | 13.3 (August 2020) | 13.4, 13.5 | [13.7 (December 2020)](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5722) | 14.0 (May 2021?) |
-| ------ | --------------- | ---------------- | ---------------- | ------------------ | ------------ | -------------------- | ---------------- |
-| PG11 | MRs/`master`/`2-hour`/`nightly` | MRs/`master`/`2-hour`/`nightly` | MRs/`master`/`2-hour`/`nightly` | MRs/`master`/`2-hour`/`nightly` | MRs/`master`/`2-hour`/`nightly` | `nightly` | - |
-| PG12 | - | - | `nightly` | `2-hour`/`nightly` | `2-hour`/`nightly` | MRs/`2-hour`/`nightly` | `2-hour`/`nightly` |
-| PG13 | - | - | - | - | - | - | MRs/`2-hour`/`nightly` |
+| PostgreSQL version | 13.7 (December 2020) | 13.8 (January 2021) | 13.9 (February 2021) | 13.10 (March 2021) | 13.11 (April 2021) | 14.0 (May 2021?) |
+| -------------------| -------------------- | ------------------- | -------------------- | ------------------ | ------------------ | ---------------- |
+| PG11 | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` |
+| PG12 | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` |
### Test jobs
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index f6c7f493b21..ce9c1191648 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -128,6 +128,62 @@ console.
As a follow up to finding `N+1` queries with Bullet, consider writing a [QueryRecoder test](query_recorder.md) to prevent a regression.
+## System stats
+
+During or after profiling, you may want to get detailed information about the Ruby virtual machine process,
+such as memory consumption, time spent on CPU, or garbage collector statistics. These are easy to produce individually
+through various tools, but for convenience, a summary endpoint has been added that exports this data as a JSON payload:
+
+```shell
+curl localhost:3000/-/metrics/system | jq
+```
+
+Example output:
+
+```json
+{
+ "version": "ruby 2.7.2p137 (2020-10-01 revision a8323b79eb) [x86_64-linux-gnu]",
+ "gc_stat": {
+ "count": 118,
+ "heap_allocated_pages": 11503,
+ "heap_sorted_length": 11503,
+ "heap_allocatable_pages": 0,
+ "heap_available_slots": 4688580,
+ "heap_live_slots": 3451712,
+ "heap_free_slots": 1236868,
+ "heap_final_slots": 0,
+ "heap_marked_slots": 3451450,
+ "heap_eden_pages": 11503,
+ "heap_tomb_pages": 0,
+ "total_allocated_pages": 11503,
+ "total_freed_pages": 0,
+ "total_allocated_objects": 32679478,
+ "total_freed_objects": 29227766,
+ "malloc_increase_bytes": 84760,
+ "malloc_increase_bytes_limit": 32883343,
+ "minor_gc_count": 88,
+ "major_gc_count": 30,
+ "compact_count": 0,
+ "remembered_wb_unprotected_objects": 114228,
+ "remembered_wb_unprotected_objects_limit": 228456,
+ "old_objects": 3185330,
+ "old_objects_limit": 6370660,
+ "oldmalloc_increase_bytes": 21838024,
+ "oldmalloc_increase_bytes_limit": 119181499
+ },
+ "memory_rss": 1326501888,
+ "memory_uss": 1048563712,
+ "memory_pss": 1139554304,
+ "time_cputime": 82.885264633,
+ "time_realtime": 1610459445.5579069,
+ "time_monotonic": 24001.23145713,
+ "worker_id": "puma_0"
+}
+```
+
+NOTE:
+This endpoint is only available for Rails web workers. Sidekiq workers can not be inspected this way.
+
## Settings that impact performance
### Application settings
diff --git a/doc/user/project/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md
index 23ad881087e..40a4631694b 100644
--- a/doc/user/project/merge_requests/revert_changes.md
+++ b/doc/user/project/merge_requests/revert_changes.md
@@ -59,10 +59,6 @@ mainline:
git revert -m 2 7a39eb0
```
-From [GitLab 13.8 onwards](https://gitlab.com/gitlab-org/gitlab/-/issues/35824), merge requests
-reverted by another merge request through one of the methods described in this document
-will display a link to the reverted merge request at the top-left corner within the **Merged** badge.
-
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
diff --git a/lib/atlassian/jira_connect/client.rb b/lib/atlassian/jira_connect/client.rb
index 76211eba0bb..5f7550bdbb4 100644
--- a/lib/atlassian/jira_connect/client.rb
+++ b/lib/atlassian/jira_connect/client.rb
@@ -17,12 +17,14 @@ module Atlassian
dev_info = args.slice(:commits, :branches, :merge_requests)
build_info = args.slice(:pipelines)
deploy_info = args.slice(:deployments)
+ ff_info = args.slice(:feature_flags)
responses = []
responses << store_dev_info(**common, **dev_info) if dev_info.present?
responses << store_build_info(**common, **build_info) if build_info.present?
responses << store_deploy_info(**common, **deploy_info) if deploy_info.present?
+ responses << store_ff_info(**common, **ff_info) if ff_info.present?
raise ArgumentError, 'Invalid arguments' if responses.empty?
responses.compact
@@ -30,6 +32,20 @@ module Atlassian
private
+ def store_ff_info(project:, feature_flags:, **opts)
+ return unless Feature.enabled?(:jira_sync_feature_flags, project)
+
+ items = feature_flags.map { |flag| Serializers::FeatureFlagEntity.represent(flag, opts) }
+ items.reject! { |item| item.issue_keys.empty? }
+
+ return if items.empty?
+
+ post('/rest/featureflags/0.1/bulk', {
+ flags: items,
+ properties: { projectId: "project-#{project.id}" }
+ })
+ end
+
def store_deploy_info(project:, deployments:, **opts)
return unless Feature.enabled?(:jira_sync_deployments, project)
diff --git a/lib/atlassian/jira_connect/serializers/feature_flag_entity.rb b/lib/atlassian/jira_connect/serializers/feature_flag_entity.rb
new file mode 100644
index 00000000000..e17c150aacb
--- /dev/null
+++ b/lib/atlassian/jira_connect/serializers/feature_flag_entity.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+module Atlassian
+ module JiraConnect
+ module Serializers
+ class FeatureFlagEntity < Grape::Entity
+ include Gitlab::Routing
+
+ alias_method :flag, :object
+
+ format_with(:string, &:to_s)
+
+ expose :schema_version, as: :schemaVersion
+ expose :id, format_with: :string
+ expose :name, as: :key
+ expose :update_sequence_id, as: :updateSequenceId
+ expose :name, as: :displayName
+ expose :summary
+ expose :details
+ expose :issue_keys, as: :issueKeys
+
+ def issue_keys
+ @issue_keys ||= JiraIssueKeyExtractor.new(flag.description).issue_keys
+ end
+
+ def schema_version
+ '1.0'
+ end
+
+ def update_sequence_id
+ options[:update_sequence_id] || Client.generate_update_sequence_id
+ end
+
+ STRATEGY_NAMES = {
+ ::Operations::FeatureFlags::Strategy::STRATEGY_DEFAULT => 'All users',
+ ::Operations::FeatureFlags::Strategy::STRATEGY_GITLABUSERLIST => 'User List',
+ ::Operations::FeatureFlags::Strategy::STRATEGY_GRADUALROLLOUTUSERID => 'Percent of users',
+ ::Operations::FeatureFlags::Strategy::STRATEGY_FLEXIBLEROLLOUT => 'Percent rollout',
+ ::Operations::FeatureFlags::Strategy::STRATEGY_USERWITHID => 'User IDs'
+ }.freeze
+
+ private
+
+ # The summary does not map very well to our FeatureFlag model.
+ #
+ # We allow feature flags to have multiple strategies, depending
+ # on the environment. Jira expects a single rollout strategy.
+ #
+ # Also, we don't actually support showing a single flag, so we use the
+ # edit path as an interim solution.
+ def summary(strategies = flag.strategies)
+ {
+ url: project_url(flag.project) + "/-/feature_flags/#{flag.id}/edit",
+ lastUpdated: flag.updated_at.iso8601,
+ status: {
+ enabled: flag.active,
+ defaultValue: '',
+ rollout: {
+ percentage: strategies.map do |s|
+ s.parameters['rollout'] || s.parameters['percentage']
+ end.compact.first&.to_f,
+ text: strategies.map { |s| STRATEGY_NAMES[s.name] }.compact.join(', ')
+ }.compact
+ }
+ }
+ end
+
+ def details
+ envs = flag.strategies.flat_map do |s|
+ s.scopes.map do |es|
+ env_type = es.environment_scope.scan(/development|testing|staging|production/).first
+ [es.environment_scope, env_type, s]
+ end
+ end
+
+ envs.map do |env_name, env_type, strat|
+ summary([strat]).merge(environment: { name: env_name, type: env_type }.compact)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb
index e78e29422ea..94e71318607 100644
--- a/lib/gitlab/ci/features.rb
+++ b/lib/gitlab/ci/features.rb
@@ -56,7 +56,7 @@ module Gitlab
end
def self.pipeline_open_merge_requests?(project)
- ::Feature.enabled?(:ci_pipeline_open_merge_requests, project, default_enabled: false)
+ ::Feature.enabled?(:ci_pipeline_open_merge_requests, project, default_enabled: true)
end
def self.ci_pipeline_editor_page_enabled?(project)
diff --git a/lib/gitlab/ci/lint.rb b/lib/gitlab/ci/lint.rb
index fb795152abe..bc8ce428eec 100644
--- a/lib/gitlab/ci/lint.rb
+++ b/lib/gitlab/ci/lint.rb
@@ -99,7 +99,8 @@ module Gitlab
except: job[:except],
environment: job[:environment],
when: job[:when],
- allow_failure: job[:allow_failure]
+ allow_failure: job[:allow_failure],
+ needs: job.dig(:needs_attributes)
}
end
end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index 43005303dec..9bbcd1e056c 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -17,6 +17,20 @@ module Gitlab
RSS_PATTERN = /VmRSS:\s+(?<value>\d+)/.freeze
MAX_OPEN_FILES_PATTERN = /Max open files\s*(?<value>\d+)/.freeze
+ def self.summary
+ proportional_mem = memory_usage_uss_pss
+ {
+ version: RUBY_DESCRIPTION,
+ gc_stat: GC.stat,
+ memory_rss: memory_usage_rss,
+ memory_uss: proportional_mem[:uss],
+ memory_pss: proportional_mem[:pss],
+ time_cputime: cpu_time,
+ time_realtime: real_time,
+ time_monotonic: monotonic_time
+ }
+ end
+
# Returns the current process' RSS (resident set size) in bytes.
def self.memory_usage_rss
sum_matches(PROC_STATUS_PATH, rss: RSS_PATTERN)[:rss].kilobytes
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 34d6742bd59..4951cd4aa3d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1265,6 +1265,9 @@ msgstr ""
msgid "A group represents your organization in GitLab. Groups allow you to manage users and collaborate across multiple projects."
msgstr ""
+msgid "A job artifact is an archive of files and directories saved by a job when it finishes."
+msgstr ""
+
msgid "A limit of %{ci_project_subscriptions_limit} subscriptions to or from a project applies."
msgstr ""
@@ -15479,6 +15482,9 @@ msgstr ""
msgid "Invite member"
msgstr ""
+msgid "Invite members"
+msgstr ""
+
msgid "Invite team members"
msgstr ""
@@ -16172,6 +16178,9 @@ msgstr ""
msgid "K8s pod health"
msgstr ""
+msgid "Keep artifacts from most recent successful jobs"
+msgstr ""
+
msgid "Keep divergent refs"
msgstr ""
@@ -16962,9 +16971,6 @@ msgstr ""
msgid "MERGED"
msgstr ""
-msgid "MERGED (REVERTED)"
-msgstr ""
-
msgid "MR widget|Back to the Merge request"
msgstr ""
@@ -17721,18 +17727,9 @@ msgstr ""
msgid "MergeRequest|Search files (%{modifier_key}P)"
msgstr ""
-msgid "MergeRequest|reverted"
-msgstr ""
-
msgid "Merged"
msgstr ""
-msgid "Merged (%{linkStart}reverted%{linkEnd})"
-msgstr ""
-
-msgid "Merged (%{reverted})"
-msgstr ""
-
msgid "Merged MRs"
msgstr ""
@@ -25851,9 +25848,6 @@ msgstr ""
msgid "Sherlock Transactions"
msgstr ""
-msgid "Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{b_start}will%{b_end} lose access to your account."
-msgstr ""
-
msgid "Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %{boldStart}will%{boldEnd} lose access to your account."
msgstr ""
@@ -28061,6 +28055,9 @@ msgstr ""
msgid "The issue was successfully promoted to an epic. Redirecting to epic..."
msgstr ""
+msgid "The latest artifacts created by jobs in the most recent successful pipeline will be stored."
+msgstr ""
+
msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc."
msgstr ""
@@ -28397,6 +28394,9 @@ msgstr ""
msgid "There was a problem fetching project users."
msgstr ""
+msgid "There was a problem fetching the keep latest artifact setting."
+msgstr ""
+
msgid "There was a problem fetching users."
msgstr ""
@@ -28409,6 +28409,9 @@ msgstr ""
msgid "There was a problem sending the confirmation email"
msgstr ""
+msgid "There was a problem updating the keep latest artifact setting."
+msgstr ""
+
msgid "There was an error %{message} todo."
msgstr ""
diff --git a/package.json b/package.json
index 14cb6d04d8f..230625fc09d 100644
--- a/package.json
+++ b/package.json
@@ -163,7 +163,7 @@
},
"devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.10.1",
- "@gitlab/eslint-plugin": "^6.0.0",
+ "@gitlab/eslint-plugin": "6.0.0",
"@testing-library/dom": "^7.16.2",
"@vue/test-utils": "1.1.2",
"acorn": "^6.3.0",
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index f350d7378dc..9fa90dde997 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -28,8 +28,38 @@ RSpec.describe MetricsController, :request_store do
end
end
+ shared_examples_for 'protected metrics endpoint' do |examples|
+ context 'accessed from whitelisted ip' do
+ before do
+ allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(whitelisted_ip)
+ end
+
+ it_behaves_like examples
+ end
+
+ context 'accessed from ip in whitelisted range' do
+ before do
+ allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(ip_in_whitelisted_range)
+ end
+
+ it_behaves_like examples
+ end
+
+ context 'accessed from not whitelisted ip' do
+ before do
+ allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(not_whitelisted_ip)
+ end
+
+ it 'returns the expected error response' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
describe '#index' do
- shared_examples_for 'endpoint providing metrics' do
+ shared_examples_for 'providing metrics' do
it 'returns prometheus metrics' do
get :index
@@ -51,32 +81,35 @@ RSpec.describe MetricsController, :request_store do
end
end
- context 'accessed from whitelisted ip' do
- before do
- allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(whitelisted_ip)
- end
-
- it_behaves_like 'endpoint providing metrics'
- end
+ include_examples 'protected metrics endpoint', 'providing metrics'
+ end
- context 'accessed from ip in whitelisted range' do
- before do
- allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(ip_in_whitelisted_range)
+ describe '#system' do
+ shared_examples_for 'providing system stats' do
+ let(:summary) do
+ {
+ version: 'ruby-3.0-patch1',
+ memory_rss: 1024
+ }
end
- it_behaves_like 'endpoint providing metrics'
- end
+ it 'renders system stats JSON' do
+ expect(Prometheus::PidProvider).to receive(:worker_id).and_return('worker-0')
+ expect(Gitlab::Metrics::System).to receive(:summary).and_return(summary)
- context 'accessed from not whitelisted ip' do
- before do
- allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(not_whitelisted_ip)
- end
-
- it 'returns the expected error response' do
- get :index
+ get :system
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response_json['version']).to eq('ruby-3.0-patch1')
+ expect(response_json['worker_id']).to eq('worker-0')
+ expect(response_json['memory_rss']).to eq(1024)
end
end
+
+ include_examples 'protected metrics endpoint', 'providing system stats'
+ end
+
+ def response_json
+ Gitlab::Json.parse(response.body)
end
end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 0e350a5e12e..a8e18385bd2 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -175,6 +175,7 @@ RSpec.describe 'Admin Groups' do
describe 'add admin himself to a group' do
before do
+ stub_feature_flags(invite_members_group_modal: false)
group.add_user(:user, Gitlab::Access::OWNER)
end
diff --git a/spec/features/groups/members/manage_groups_spec.rb b/spec/features/groups/members/manage_groups_spec.rb
index 31a2c868cac..e9bbe9de3c9 100644
--- a/spec/features/groups/members/manage_groups_spec.rb
+++ b/spec/features/groups/members/manage_groups_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe 'Groups > Members > Manage groups', :js do
let_it_be(:group_to_add) { create(:group) }
before do
+ stub_feature_flags(invite_members_group_modal: false)
group.add_owner(user)
visit group_group_members_path(group)
end
diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb
index e6da05c4873..c27d0afba6f 100644
--- a/spec/features/groups/members/manage_members_spec.rb
+++ b/spec/features/groups/members/manage_members_spec.rb
@@ -11,9 +11,37 @@ RSpec.describe 'Groups > Members > Manage members' do
let(:group) { create(:group) }
before do
+ stub_feature_flags(invite_members_group_modal: false)
sign_in(user1)
end
+ shared_examples 'includes the correct Invite Members link' do |should_include, should_not_include|
+ it 'includes either the form or the modal trigger' do
+ group.add_owner(user1)
+
+ visit group_group_members_path(group)
+
+ expect(page).to have_selector(should_include)
+ expect(page).not_to have_selector(should_not_include)
+ end
+ end
+
+ context 'when Invite Members modal is enabled' do
+ before do
+ stub_feature_flags(invite_members_group_modal: true)
+ end
+
+ it_behaves_like 'includes the correct Invite Members link', '.js-invite-members-trigger', '.invite-users-form'
+ end
+
+ context 'when Invite Members modal is disabled' do
+ before do
+ stub_feature_flags(invite_members_group_modal: false)
+ end
+
+ it_behaves_like 'includes the correct Invite Members link', '.invite-users-form', '.js-invite-members-trigger'
+ end
+
it 'update user to owner level', :js do
group.add_owner(user1)
group.add_developer(user2)
diff --git a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
index de9b32e00aa..38deee547a3 100644
--- a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe 'Groups > Members > Owner adds member with expiration date', :js
let(:new_member) { create(:user, name: 'Mary Jane') }
before do
+ stub_feature_flags(invite_members_group_modal: false)
group.add_owner(user1)
sign_in(user1)
end
diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb
index 9c740fd3834..aa15f04bf24 100644
--- a/spec/features/projects/members/group_members_spec.rb
+++ b/spec/features/projects/members/group_members_spec.rb
@@ -111,6 +111,7 @@ RSpec.describe 'Projects members', :js do
context 'with a group requester' do
before do
+ stub_feature_flags(invite_members_group_modal: false)
group.request_access(group_requester)
visit project_project_members_path(project)
end
diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb
index eba0867dc8c..62115f2dce6 100644
--- a/spec/features/projects/members/list_spec.rb
+++ b/spec/features/projects/members/list_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe 'Project members list' do
let(:project) { create(:project, namespace: group) }
before do
+ stub_feature_flags(invite_members_group_modal: false)
sign_in(user1)
group.add_owner(user1)
end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index e4a8d836413..5a537c1d4df 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -742,65 +742,30 @@ RSpec.describe 'Login' do
end
context 'when the user did not enable 2FA' do
- context 'when `vue_2fa_recovery_codes` feature flag is disabled' do
- before do
- stub_feature_flags(vue_2fa_recovery_codes: false)
- end
-
- it 'asks to set 2FA before asking to accept the terms' do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
-
- visit new_user_session_path
-
- fill_in 'user_login', with: user.email
- fill_in 'user_password', with: '12345678'
-
- click_button 'Sign in'
-
- expect_to_be_on_terms_page
- click_button 'Accept terms'
-
- expect(current_path).to eq(profile_two_factor_auth_path)
-
- fill_in 'pin_code', with: user.reload.current_otp
-
- click_button 'Register with two-factor app'
-
- expect(page).to have_content('Congratulations! You have enabled Two-factor Authentication!')
-
- click_link 'Proceed'
-
- expect(current_path).to eq(profile_account_path)
- end
- end
-
- context 'when `vue_2fa_recovery_codes` feature flag is enabled' do
- it 'asks to set 2FA before asking to accept the terms', :js do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
+ it 'asks to set 2FA before asking to accept the terms', :js do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
- visit new_user_session_path
+ visit new_user_session_path
- fill_in 'user_login', with: user.email
- fill_in 'user_password', with: '12345678'
+ fill_in 'user_login', with: user.email
+ fill_in 'user_password', with: '12345678'
- click_button 'Sign in'
+ click_button 'Sign in'
- expect_to_be_on_terms_page
- click_button 'Accept terms'
+ expect_to_be_on_terms_page
+ click_button 'Accept terms'
- expect(current_path).to eq(profile_two_factor_auth_path)
+ expect(current_path).to eq(profile_two_factor_auth_path)
- fill_in 'pin_code', with: user.reload.current_otp
+ fill_in 'pin_code', with: user.reload.current_otp
- click_button 'Register with two-factor app'
- click_button 'Copy codes'
- click_link 'Proceed'
+ click_button 'Register with two-factor app'
+ click_button 'Copy codes'
+ click_link 'Proceed'
- expect(current_path).to eq(profile_account_path)
- expect(page).to have_content('Congratulations! You have enabled Two-factor Authentication!')
- end
+ expect(current_path).to eq(profile_account_path)
+ expect(page).to have_content('Congratulations! You have enabled Two-factor Authentication!')
end
end
diff --git a/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap b/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
new file mode 100644
index 00000000000..9d05e6d99f6
--- /dev/null
+++ b/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Keep latest artifact checkbox sets correct setting value in checkbox with query result 1`] = `
+<div>
+ <!---->
+
+ <gl-form-checkbox-stub
+ checked="true"
+ >
+ <b
+ class="gl-mr-3"
+ >
+ Keep artifacts from most recent successful jobs
+ </b>
+
+ <gl-link-stub
+ href="/help/ci/pipelines/job_artifacts"
+ >
+ More information
+ </gl-link-stub>
+ </gl-form-checkbox-stub>
+
+ <p>
+
+ The latest artifacts created by jobs in the most recent successful pipeline will be stored.
+
+ </p>
+</div>
+`;
diff --git a/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
new file mode 100644
index 00000000000..f13196d89b5
--- /dev/null
+++ b/spec/frontend/artifacts_settings/components/keep_latest_artifact_checkbox_spec.js
@@ -0,0 +1,62 @@
+import { GlFormCheckbox, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import KeepLatestArtifactCheckbox from '~/artifacts_settings/keep_latest_artifact_checkbox.vue';
+import UpdateKeepLatestArtifactProjectSetting from '~/artifacts_settings/graphql/mutations/update_keep_latest_artifact_project_setting.mutation.graphql';
+
+describe('Keep latest artifact checkbox', () => {
+ let wrapper;
+
+ const mutate = jest.fn().mockResolvedValue();
+ const fullPath = 'gitlab-org/gitlab';
+ const helpPagePath = '/help/ci/pipelines/job_artifacts';
+
+ const findCheckbox = () => wrapper.find(GlFormCheckbox);
+ const findHelpLink = () => wrapper.find(GlLink);
+
+ const createComponent = () => {
+ wrapper = shallowMount(KeepLatestArtifactCheckbox, {
+ provide: {
+ fullPath,
+ helpPagePath,
+ },
+ mocks: {
+ $apollo: {
+ mutate,
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('displays the checkbox and the help link', () => {
+ expect(findCheckbox().exists()).toBe(true);
+ expect(findHelpLink().exists()).toBe(true);
+ });
+
+ it('sets correct setting value in checkbox with query result', async () => {
+ await wrapper.setData({ keepLatestArtifact: true });
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('calls mutation on artifact setting change with correct payload', () => {
+ findCheckbox().vm.$emit('change', false);
+
+ const expected = {
+ mutation: UpdateKeepLatestArtifactProjectSetting,
+ variables: {
+ fullPath,
+ keepLatestArtifact: false,
+ },
+ };
+
+ expect(mutate).toHaveBeenCalledWith(expected);
+ });
+});
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index e671a2dfb57..29b234962f8 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -159,7 +159,7 @@ describe('BoardForm', () => {
beforeEach(() => {
mutate = jest.fn().mockResolvedValue({
data: {
- createBoard: { board: { id: 'gid://gitlab/Board/123' } },
+ createBoard: { board: { id: 'gid://gitlab/Board/123', webPath: 'test-path' } },
},
});
});
@@ -174,7 +174,6 @@ describe('BoardForm', () => {
});
it('calls a correct GraphQL mutation and redirects to correct page from existing board', async () => {
- window.location = new URL('https://test/boards/1');
createComponent({ canAdminBoard: true });
fillForm();
@@ -190,27 +189,7 @@ describe('BoardForm', () => {
});
await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith('123');
- });
-
- it('calls a correct GraphQL mutation and redirects to correct page from boards list', async () => {
- window.location = new URL('https://test/boards');
- createComponent({ canAdminBoard: true });
- fillForm();
-
- await waitForPromises();
-
- expect(mutate).toHaveBeenCalledWith({
- mutation: createBoardMutation,
- variables: {
- input: expect.objectContaining({
- name: 'test',
- }),
- },
- });
-
- await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith('boards/123');
+ expect(visitUrl).toHaveBeenCalledWith('test-path');
});
it('shows an error flash if GraphQL mutation fails', async () => {
@@ -261,10 +240,10 @@ describe('BoardForm', () => {
});
});
- it('calls GraphQL mutation with correct parameters', async () => {
+ it('calls GraphQL mutation with correct parameters when issues are not grouped', async () => {
mutate = jest.fn().mockResolvedValue({
data: {
- updateBoard: { board: { id: 'gid://gitlab/Board/321' } },
+ updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
},
});
window.location = new URL('https://test/boards/1');
@@ -284,7 +263,33 @@ describe('BoardForm', () => {
});
await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith('321');
+ expect(visitUrl).toHaveBeenCalledWith('test-path');
+ });
+
+ it('calls GraphQL mutation with correct parameters when issues are grouped by epic', async () => {
+ mutate = jest.fn().mockResolvedValue({
+ data: {
+ updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
+ },
+ });
+ window.location = new URL('https://test/boards/1?group_by=epic');
+ createComponent({ canAdminBoard: true });
+
+ findInput().trigger('keyup.enter', { metaKey: true });
+
+ await waitForPromises();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: updateBoardMutation,
+ variables: {
+ input: expect.objectContaining({
+ id: `gid://gitlab/Board/${currentBoard.id}`,
+ }),
+ },
+ });
+
+ await waitForPromises();
+ expect(visitUrl).toHaveBeenCalledWith('test-path?group_by=epic');
});
it('shows an error flash if GraphQL mutation fails', async () => {
diff --git a/spec/frontend/ide/lib/languages/hcl_spec.js b/spec/frontend/ide/lib/languages/hcl_spec.js
index a39673a3225..c6ebad6a4f4 100644
--- a/spec/frontend/ide/lib/languages/hcl_spec.js
+++ b/spec/frontend/ide/lib/languages/hcl_spec.js
@@ -284,6 +284,42 @@ describe('tokenization for .tf files', () => {
],
],
],
+ [
+ ` foo = <<-EOF
+ bar
+ EOF`,
+ [
+ [
+ { language: 'hcl', offset: 0, type: '' },
+ { language: 'hcl', offset: 2, type: 'variable.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'operator.hcl' },
+ { language: 'hcl', offset: 7, type: '' },
+ { language: 'hcl', offset: 8, type: 'string.heredoc.delimiter.hcl' },
+ ],
+ [{ language: 'hcl', offset: 0, type: 'string.heredoc.hcl' }],
+ [
+ { language: 'hcl', offset: 0, type: 'string.heredoc.hcl' },
+ { language: 'hcl', offset: 2, type: 'string.heredoc.delimiter.hcl' },
+ ],
+ ],
+ ],
+ [
+ `foo = <<-EOF
+bar
+EOF`,
+ [
+ [
+ { language: 'hcl', offset: 0, type: 'variable.hcl' },
+ { language: 'hcl', offset: 3, type: '' },
+ { language: 'hcl', offset: 4, type: 'operator.hcl' },
+ { language: 'hcl', offset: 5, type: '' },
+ { language: 'hcl', offset: 6, type: 'string.heredoc.delimiter.hcl' },
+ ],
+ [{ language: 'hcl', offset: 0, type: 'string.heredoc.hcl' }],
+ [{ language: 'hcl', offset: 0, type: 'string.heredoc.delimiter.hcl' }],
+ ],
+ ],
])('%s', (string, tokens) => {
expect(editor.tokenize(string, 'hcl')).toEqual(tokens);
});
diff --git a/spec/frontend/merge_request/components/status_box_spec.js b/spec/frontend/merge_request/components/status_box_spec.js
index b1f0219643a..e6b6512476b 100644
--- a/spec/frontend/merge_request/components/status_box_spec.js
+++ b/spec/frontend/merge_request/components/status_box_spec.js
@@ -42,7 +42,6 @@ describe('Merge request status box component', () => {
it('renders human readable test', () => {
factory({
initialState: testCase.state,
- initialIsReverted: false,
});
expect(wrapper.text()).toContain(testCase.name);
@@ -51,7 +50,6 @@ describe('Merge request status box component', () => {
it('sets css class', () => {
factory({
initialState: testCase.state,
- initialIsReverted: false,
});
expect(wrapper.classes()).toContain(testCase.class);
@@ -60,7 +58,6 @@ describe('Merge request status box component', () => {
it('renders icon', () => {
factory({
initialState: testCase.state,
- initialIsReverted: false,
});
expect(wrapper.find('[data-testid="status-icon"]').props('name')).toBe(testCase.icon);
@@ -68,29 +65,14 @@ describe('Merge request status box component', () => {
});
});
- describe('when merge request is reverted', () => {
- it('renders a link to the reverted merge request', () => {
- factory({
- initialState: 'merged',
- initialIsReverted: true,
- initialRevertedPath: 'http://test.com',
- });
-
- expect(wrapper.find('[data-testid="reverted-link"]').attributes('href')).toBe(
- 'http://test.com',
- );
- });
- });
-
it('updates with eventhub event', async () => {
factory({
initialState: 'opened',
- initialIsReverted: false,
});
expect(wrapper.text()).toContain('Open');
- mrEventHub.$emit('mr.state.updated', { state: 'closed', reverted: false });
+ mrEventHub.$emit('mr.state.updated', { state: 'closed' });
await nextTick();
diff --git a/spec/graphql/resolvers/ci/config_resolver_spec.rb b/spec/graphql/resolvers/ci/config_resolver_spec.rb
index 66b66bbceda..ca7ae73fef8 100644
--- a/spec/graphql/resolvers/ci/config_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/config_resolver_spec.rb
@@ -7,10 +7,10 @@ RSpec.describe Resolvers::Ci::ConfigResolver do
describe '#resolve' do
before do
- yaml_processor_double = instance_double(::Gitlab::Ci::YamlProcessor)
- allow(yaml_processor_double).to receive(:execute).and_return(fake_result)
+ ci_lint_double = instance_double(::Gitlab::Ci::Lint)
+ allow(ci_lint_double).to receive(:validate).and_return(fake_result)
- allow(::Gitlab::Ci::YamlProcessor).to receive(:new).and_return(yaml_processor_double)
+ allow(::Gitlab::Ci::Lint).to receive(:new).and_return(ci_lint_double)
end
let_it_be(:user) { create(:user) }
@@ -24,8 +24,9 @@ RSpec.describe Resolvers::Ci::ConfigResolver do
context 'with a valid .gitlab-ci.yml' do
let(:fake_result) do
- ::Gitlab::Ci::YamlProcessor::Result.new(
- ci_config: ::Gitlab::Ci::Config.new(content),
+ ::Gitlab::Ci::Lint::Result.new(
+ merged_yaml: content,
+ jobs: [],
errors: [],
warnings: []
)
@@ -45,8 +46,9 @@ RSpec.describe Resolvers::Ci::ConfigResolver do
let(:content) { 'invalid' }
let(:fake_result) do
- Gitlab::Ci::YamlProcessor::Result.new(
- ci_config: nil,
+ Gitlab::Ci::Lint::Result.new(
+ jobs: [],
+ merged_yaml: content,
errors: ['Invalid configuration format'],
warnings: []
)
diff --git a/spec/graphql/types/ci/config/job_restriction_type_spec.rb b/spec/graphql/types/ci/config/job_restriction_type_spec.rb
new file mode 100644
index 00000000000..dd46a38b7c2
--- /dev/null
+++ b/spec/graphql/types/ci/config/job_restriction_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Types::Ci::Config::JobRestrictionType do
+ specify { expect(described_class.graphql_name).to eq('CiConfigJobRestriction') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[refs]
+
+ expect(described_class).to include_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/ci/config/job_type_spec.rb b/spec/graphql/types/ci/config/job_type_spec.rb
index 600d665a84b..de4e167f69c 100644
--- a/spec/graphql/types/ci/config/job_type_spec.rb
+++ b/spec/graphql/types/ci/config/job_type_spec.rb
@@ -7,10 +7,19 @@ RSpec.describe Types::Ci::Config::JobType do
it 'exposes the expected fields' do
expected_fields = %i[
+ afterScript
+ allowFailure
+ beforeScript
+ environment
+ except
+ script
name
+ only
group_name
stage
+ tags
needs
+ when
]
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/lib/atlassian/jira_connect/client_spec.rb b/spec/lib/atlassian/jira_connect/client_spec.rb
index 9759bceb58e..e042b2b183d 100644
--- a/spec/lib/atlassian/jira_connect/client_spec.rb
+++ b/spec/lib/atlassian/jira_connect/client_spec.rb
@@ -31,7 +31,13 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
describe '#send_info' do
- it 'calls store_deploy_info, store_build_info and store_dev_info as appropriate' do
+ it 'calls more specific methods as appropriate' do
+ expect(subject).to receive(:store_ff_info).with(
+ project: project,
+ update_sequence_id: :x,
+ feature_flags: :r
+ ).and_return(:ff_stored)
+
expect(subject).to receive(:store_build_info).with(
project: project,
update_sequence_id: :x,
@@ -59,11 +65,12 @@ RSpec.describe Atlassian::JiraConnect::Client do
branches: :b,
merge_requests: :c,
pipelines: :y,
- deployments: :q
+ deployments: :q,
+ feature_flags: :r
}
expect(subject.send_info(**args))
- .to contain_exactly(:dev_stored, :build_stored, :deploys_stored)
+ .to contain_exactly(:dev_stored, :build_stored, :deploys_stored, :ff_stored)
end
it 'only calls methods that we need to call' do
@@ -158,6 +165,64 @@ RSpec.describe Atlassian::JiraConnect::Client do
end
end
+ describe '#store_ff_info' do
+ let_it_be(:feature_flags) { create_list(:operations_feature_flag, 3, project: project) }
+
+ let(:schema) do
+ Atlassian::Schemata.ff_info_payload
+ end
+
+ let(:body) do
+ matcher = be_valid_json.and match_schema(schema)
+
+ ->(text) { matcher.matches?(text) }
+ end
+
+ before do
+ feature_flags.first.update!(description: 'RELEVANT-123')
+ feature_flags.second.update!(description: 'RELEVANT-123')
+ path = '/rest/featureflags/0.1/bulk'
+ stub_full_request('https://gitlab-test.atlassian.net' + path, method: :post)
+ .with(body: body, headers: expected_headers(path))
+ end
+
+ it "calls the API with auth headers" do
+ subject.send(:store_ff_info, project: project, feature_flags: feature_flags)
+ end
+
+ it 'only sends information about relevant MRs' do
+ expect(subject).to receive(:post).with('/rest/featureflags/0.1/bulk', {
+ flags: have_attributes(size: 2), properties: Hash
+ })
+
+ subject.send(:store_ff_info, project: project, feature_flags: feature_flags)
+ end
+
+ it 'does not call the API if there is nothing to report' do
+ expect(subject).not_to receive(:post)
+
+ subject.send(:store_ff_info, project: project, feature_flags: [feature_flags.last])
+ end
+
+ it 'does not call the API if the feature flag is not enabled' do
+ stub_feature_flags(jira_sync_feature_flags: false)
+
+ expect(subject).not_to receive(:post)
+
+ subject.send(:store_ff_info, project: project, feature_flags: feature_flags)
+ end
+
+ it 'does call the API if the feature flag enabled for the project' do
+ stub_feature_flags(jira_sync_feature_flags: project)
+
+ expect(subject).to receive(:post).with('/rest/featureflags/0.1/bulk', {
+ flags: Array, properties: Hash
+ })
+
+ subject.send(:store_ff_info, project: project, feature_flags: feature_flags)
+ end
+ end
+
describe '#store_build_info' do
let(:build_info_payload_schema) do
Atlassian::Schemata.build_info_payload
diff --git a/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
new file mode 100644
index 00000000000..964801338cf
--- /dev/null
+++ b/spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Atlassian::JiraConnect::Serializers::FeatureFlagEntity do
+ let_it_be(:user) { create_default(:user) }
+ let_it_be(:project) { create_default(:project) }
+
+ subject { described_class.represent(feature_flag) }
+
+ context 'when the feature flag does not belong to any Jira issue' do
+ let_it_be(:feature_flag) { create(:operations_feature_flag) }
+
+ describe '#issue_keys' do
+ it 'is empty' do
+ expect(subject.issue_keys).to be_empty
+ end
+ end
+
+ describe '#to_json' do
+ it 'can encode the object' do
+ expect(subject.to_json).to be_valid_json
+ end
+
+ it 'is invalid, since it has no issue keys' do
+ expect(subject.to_json).not_to match_schema(Atlassian::Schemata.feature_flag_info)
+ end
+ end
+ end
+
+ context 'when the feature flag does belong to a Jira issue' do
+ let(:feature_flag) do
+ create(:operations_feature_flag, description: 'THING-123')
+ end
+
+ describe '#issue_keys' do
+ it 'is not empty' do
+ expect(subject.issue_keys).not_to be_empty
+ end
+ end
+
+ describe '#to_json' do
+ it 'is valid according to the feature flag info schema' do
+ expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.feature_flag_info)
+ end
+ end
+
+ context 'it has a percentage strategy' do
+ let!(:scopes) do
+ strat = create(:operations_strategy,
+ feature_flag: feature_flag,
+ name: ::Operations::FeatureFlags::Strategy::STRATEGY_GRADUALROLLOUTUSERID,
+ parameters: { 'percentage' => '50', 'groupId' => 'abcde' })
+
+ [
+ create(:operations_scope, strategy: strat, environment_scope: 'production in live'),
+ create(:operations_scope, strategy: strat, environment_scope: 'staging'),
+ create(:operations_scope, strategy: strat)
+ ]
+ end
+
+ let(:entity) { Gitlab::Json.parse(subject.to_json) }
+
+ it 'is valid according to the feature flag info schema' do
+ expect(subject.to_json).to be_valid_json.and match_schema(Atlassian::Schemata.feature_flag_info)
+ end
+
+ it 'has the correct summary' do
+ expect(entity.dig('summary', 'status')).to eq(
+ 'enabled' => true,
+ 'defaultValue' => '',
+ 'rollout' => { 'percentage' => 50.0, 'text' => 'Percent of users' }
+ )
+ end
+
+ it 'includes the correct environments' do
+ expect(entity['details']).to contain_exactly(
+ include('environment' => { 'name' => 'production in live', 'type' => 'production' }),
+ include('environment' => { 'name' => 'staging', 'type' => 'staging' }),
+ include('environment' => { 'name' => scopes.last.environment_scope })
+ )
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 720bd5d79b3..732aa553737 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -96,6 +96,25 @@ RSpec.describe Gitlab::Metrics::System do
expect(described_class.memory_usage_uss_pss).to eq(uss: 475136, pss: 515072)
end
end
+
+ describe '.summary' do
+ it 'contains a selection of the available fields' do
+ stub_const('RUBY_DESCRIPTION', 'ruby-3.0-patch1')
+ mock_existing_proc_file('/proc/self/status', proc_status)
+ mock_existing_proc_file('/proc/self/smaps_rollup', proc_smaps_rollup)
+
+ summary = described_class.summary
+
+ expect(summary[:version]).to eq('ruby-3.0-patch1')
+ expect(summary[:gc_stat].keys).to eq(GC.stat.keys)
+ expect(summary[:memory_rss]).to eq(2527232)
+ expect(summary[:memory_uss]).to eq(475136)
+ expect(summary[:memory_pss]).to eq(515072)
+ expect(summary[:time_cputime]).to be_a(Float)
+ expect(summary[:time_realtime]).to be_a(Float)
+ expect(summary[:time_monotonic]).to be_a(Float)
+ end
+ end
end
context 'when /proc files do not exist' do
@@ -128,6 +147,21 @@ RSpec.describe Gitlab::Metrics::System do
expect(described_class.max_open_file_descriptors).to eq(0)
end
end
+
+ describe '.summary' do
+ it 'returns only available fields' do
+ summary = described_class.summary
+
+ expect(summary[:version]).to be_a(String)
+ expect(summary[:gc_stat].keys).to eq(GC.stat.keys)
+ expect(summary[:memory_rss]).to eq(0)
+ expect(summary[:memory_uss]).to eq(0)
+ expect(summary[:memory_pss]).to eq(0)
+ expect(summary[:time_cputime]).to be_a(Float)
+ expect(summary[:time_realtime]).to be_a(Float)
+ expect(summary[:time_monotonic]).to be_a(Float)
+ end
+ end
end
describe '.cpu_time' do
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 47212f54df6..cfa87b3e39e 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -750,27 +750,6 @@ eos
end
end
- describe '#reverting_commit' do
- let(:user) { create(:user) }
- let(:issue) { create(:issue, author: user, project: project) }
-
- it 'returns the reverting commit' do
- create(:note_on_issue,
- noteable: issue,
- system: true,
- note: commit.revert_description(user),
- project: issue.project)
-
- expect_next_instance_of(Commit) do |revert_commit|
- expect(revert_commit).to receive(:reverts_commit?)
- .with(commit, user)
- .and_return(true)
- end
-
- expect(commit.reverting_commit(user, issue.notes_with_associations)).to eq(commit)
- end
- end
-
describe '#has_been_reverted?' do
let(:user) { create(:user) }
let(:issue) { create(:issue, author: user, project: project) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index d1f5a2c7077..1cf197322f5 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2455,85 +2455,6 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
- describe '#reverting_merge_request' do
- subject { create(:merge_request, source_project: create(:project, :repository)) }
-
- context 'when there is no merge_commit for the MR' do
- before do
- subject.metrics.update!(merged_at: Time.current.utc)
- end
-
- it 'returns nil' do
- expect(subject.reverting_merge_request(nil)).to be_nil
- end
- end
-
- context 'when the MR has been merged' do
- before do
- MergeRequests::MergeService
- .new(subject.target_project, subject.author, { sha: subject.diff_head_sha })
- .execute(subject)
- end
-
- context 'when there is no revert commit' do
- it 'returns nil' do
- expect(subject.reverting_merge_request(nil)).to be_nil
- end
- end
-
- context 'when there is no merged_at for the MR' do
- before do
- subject.metrics.update!(merged_at: nil)
- end
-
- it 'returns nil' do
- expect(subject.reverting_merge_request(nil)).to be_nil
- end
- end
-
- context 'when there is a revert commit by MR' do
- let(:current_user) { subject.author }
- let(:branch) { subject.source_branch }
- let(:project) { subject.source_project }
-
- let(:revert_commit_id) do
- params = {
- commit: subject.merge_commit,
- branch_name: branch,
- start_branch: branch
- }
-
- Commits::RevertService.new(project, current_user, params).execute[:result]
- end
-
- let(:revert_merge_request) do
- create(
- :merge_request,
- author: subject.author,
- target_project: subject.target_project,
- source_project: subject.source_project,
- merge_commit_sha: revert_commit_id,
- description: "This reverts merge request !#{subject.id}")
- end
-
- it 'returns the reverting merge request' do
- ProcessCommitWorker.new.perform(project.id,
- current_user.id,
- project.commit(revert_commit_id).to_hash,
- project.default_branch == branch)
-
- MergeRequests::MergeService.new(
- subject.target_project,
- subject.author,
- { sha: revert_merge_request.diff_head_sha }
- ).execute(revert_merge_request)
-
- expect(subject.reverting_merge_request(current_user)).to eq(revert_merge_request)
- end
- end
- end
- end
-
describe '#can_be_reverted?' do
subject { create(:merge_request, source_project: create(:project, :repository)) }
diff --git a/spec/requests/api/graphql/ci/config_spec.rb b/spec/requests/api/graphql/ci/config_spec.rb
index e53d3ab248f..8ede6e1538c 100644
--- a/spec/requests/api/graphql/ci/config_spec.rb
+++ b/spec/requests/api/graphql/ci/config_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Query.ciConfig' do
let(:query) do
%(
query {
- ciConfig(projectPath: "#{project.full_path}", content: "#{content}") {
+ ciConfig(projectPath: "#{project.full_path}", content: "#{content}", dryRun: false) {
status
errors
stages {
@@ -32,6 +32,19 @@ RSpec.describe 'Query.ciConfig' do
name
groupName
stage
+ script
+ beforeScript
+ afterScript
+ allowFailure
+ only {
+ refs
+ }
+ when
+ except {
+ refs
+ }
+ environment
+ tags
needs {
nodes {
name
@@ -77,8 +90,36 @@ RSpec.describe 'Query.ciConfig' do
{
"nodes" =>
[
- { "name" => "rspec 0 1", "groupName" => "rspec", "stage" => "build", "needs" => { "nodes" => [] } },
- { "name" => "rspec 0 2", "groupName" => "rspec", "stage" => "build", "needs" => { "nodes" => [] } }
+ {
+ "name" => "rspec 0 1",
+ "groupName" => "rspec",
+ "stage" => "build",
+ "script" => ["rake spec"],
+ "beforeScript" => ["bundle install", "bundle exec rake db:create"],
+ "afterScript" => ["echo 'run this after'"],
+ "allowFailure" => false,
+ "only" => { "refs" => %w[branches master] },
+ "when" => "on_success",
+ "except" => nil,
+ "environment" => nil,
+ "tags" => %w[ruby postgres],
+ "needs" => { "nodes" => [] }
+ },
+ {
+ "name" => "rspec 0 2",
+ "groupName" => "rspec",
+ "stage" => "build",
+ "script" => ["rake spec"],
+ "beforeScript" => ["bundle install", "bundle exec rake db:create"],
+ "afterScript" => ["echo 'run this after'"],
+ "allowFailure" => true,
+ "only" => { "refs" => %w[branches tags] },
+ "when" => "on_failure",
+ "except" => nil,
+ "environment" => nil,
+ "tags" => [],
+ "needs" => { "nodes" => [] }
+ }
]
}
},
@@ -87,7 +128,21 @@ RSpec.describe 'Query.ciConfig' do
{
"nodes" =>
[
- { "name" => "spinach", "groupName" => "spinach", "stage" => "build", "needs" => { "nodes" => [] } }
+ {
+ "name" => "spinach",
+ "groupName" => "spinach",
+ "stage" => "build",
+ "script" => ["rake spinach"],
+ "beforeScript" => ["bundle install", "bundle exec rake db:create"],
+ "afterScript" => ["echo 'run this after'"],
+ "allowFailure" => false,
+ "only" => { "refs" => %w[branches tags] },
+ "when" => "on_success",
+ "except" => { "refs" => ["tags"] },
+ "environment" => nil,
+ "tags" => [],
+ "needs" => { "nodes" => [] }
+ }
]
}
}
@@ -106,7 +161,54 @@ RSpec.describe 'Query.ciConfig' do
"jobs" =>
{
"nodes" => [
- { "name" => "docker", "groupName" => "docker", "stage" => "test", "needs" => { "nodes" => [{ "name" => "spinach" }, { "name" => "rspec 0 1" }] } }
+ {
+ "name" => "docker",
+ "groupName" => "docker",
+ "stage" => "test",
+ "script" => ["curl http://dockerhub/URL"],
+ "beforeScript" => ["bundle install", "bundle exec rake db:create"],
+ "afterScript" => ["echo 'run this after'"],
+ "allowFailure" => true,
+ "only" => { "refs" => %w[branches tags] },
+ "when" => "manual",
+ "except" => { "refs" => ["branches"] },
+ "environment" => nil,
+ "tags" => [],
+ "needs" => { "nodes" => [{ "name" => "spinach" }, { "name" => "rspec 0 1" }] }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name" => "deploy",
+ "groups" =>
+ {
+ "nodes" =>
+ [
+ {
+ "name" => "deploy_job",
+ "size" => 1,
+ "jobs" =>
+ {
+ "nodes" => [
+ {
+ "name" => "deploy_job",
+ "groupName" => "deploy_job",
+ "stage" => "deploy",
+ "script" => ["echo 'done'"],
+ "beforeScript" => ["bundle install", "bundle exec rake db:create"],
+ "afterScript" => ["echo 'run this after'"],
+ "allowFailure" => false,
+ "only" => { "refs" => %w[branches tags] },
+ "when" => "on_success",
+ "except" => nil,
+ "environment" => "production",
+ "tags" => [],
+ "needs" => { "nodes" => [] }
+ }
]
}
}
@@ -165,7 +267,21 @@ RSpec.describe 'Query.ciConfig' do
{
"nodes" =>
[
- { "name" => "build", "groupName" => "build", "stage" => "test", "needs" => { "nodes" => [] } }
+ {
+ "name" => "build",
+ "stage" => "test",
+ "groupName" => "build",
+ "script" => ["build"],
+ "afterScript" => [],
+ "beforeScript" => [],
+ "allowFailure" => false,
+ "environment" => nil,
+ "except" => nil,
+ "only" => { "refs" => %w[branches tags] },
+ "when" => "on_success",
+ "tags" => [],
+ "needs" => { "nodes" => [] }
+}
]
}
},
@@ -176,7 +292,19 @@ RSpec.describe 'Query.ciConfig' do
{
"nodes" =>
[
- { "name" => "rspec", "groupName" => "rspec", "stage" => "test", "needs" => { "nodes" => [] } }
+ { "name" => "rspec",
+ "stage" => "test",
+ "groupName" => "rspec",
+ "script" => ["rspec"],
+ "afterScript" => [],
+ "beforeScript" => [],
+ "allowFailure" => false,
+ "environment" => nil,
+ "except" => nil,
+ "only" => { "refs" => %w[branches tags] },
+ "when" => "on_success",
+ "tags" => [],
+ "needs" => { "nodes" => [] } }
]
}
}
diff --git a/spec/services/feature_flags/create_service_spec.rb b/spec/services/feature_flags/create_service_spec.rb
index 2cd19000f99..e115d8098c9 100644
--- a/spec/services/feature_flags/create_service_spec.rb
+++ b/spec/services/feature_flags/create_service_spec.rb
@@ -34,6 +34,12 @@ RSpec.describe FeatureFlags::CreateService do
it 'does not create audit log' do
expect { subject }.not_to change { AuditEvent.count }
end
+
+ it 'does not sync the feature flag to Jira' do
+ expect(::JiraConnect::SyncFeatureFlagsWorker).not_to receive(:perform_async)
+
+ subject
+ end
end
context 'when feature flag is saved correctly' do
@@ -54,6 +60,24 @@ RSpec.describe FeatureFlags::CreateService do
expect { subject }.to change { Operations::FeatureFlag.count }.by(1)
end
+ it 'syncs the feature flag to Jira' do
+ expect(::JiraConnect::SyncFeatureFlagsWorker).to receive(:perform_async).with(Integer, Integer)
+
+ subject
+ end
+
+ context 'the feature flag is disabled' do
+ before do
+ stub_feature_flags(jira_sync_feature_flags: false)
+ end
+
+ it 'does not sync the feature flag to Jira' do
+ expect(::JiraConnect::SyncFeatureFlagsWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+
it 'creates audit event' do
expected_message = 'Created feature flag <strong>feature_flag</strong> '\
'with description <strong>"description"</strong>. '\
diff --git a/spec/services/feature_flags/update_service_spec.rb b/spec/services/feature_flags/update_service_spec.rb
index 66a75a2c24e..8c4055ddd9e 100644
--- a/spec/services/feature_flags/update_service_spec.rb
+++ b/spec/services/feature_flags/update_service_spec.rb
@@ -26,6 +26,24 @@ RSpec.describe FeatureFlags::UpdateService do
expect(subject[:status]).to eq(:success)
end
+ context 'the feature flag is disabled' do
+ before do
+ stub_feature_flags(jira_sync_feature_flags: false)
+ end
+
+ it 'does not sync the feature flag to Jira' do
+ expect(::JiraConnect::SyncFeatureFlagsWorker).not_to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ it 'syncs the feature flag to Jira' do
+ expect(::JiraConnect::SyncFeatureFlagsWorker).to receive(:perform_async).with(Integer, Integer)
+
+ subject
+ end
+
it 'creates audit event with correct message' do
name_was = feature_flag.name
@@ -52,6 +70,12 @@ RSpec.describe FeatureFlags::UpdateService do
it 'does not create audit event' do
expect { subject }.not_to change { AuditEvent.count }
end
+
+ it 'does not sync the feature flag to Jira' do
+ expect(::JiraConnect::SyncFeatureFlagsWorker).not_to receive(:perform_async)
+
+ subject
+ end
end
context 'when user is reporter' do
diff --git a/spec/support/atlassian/jira_connect/schemata.rb b/spec/support/atlassian/jira_connect/schemata.rb
index 75c18a89276..d056c7cacf3 100644
--- a/spec/support/atlassian/jira_connect/schemata.rb
+++ b/spec/support/atlassian/jira_connect/schemata.rb
@@ -18,7 +18,7 @@ module Atlassian
'buildNumber' => { 'type' => 'integer' },
'updateSequenceNumber' => { 'type' => 'integer' },
'displayName' => { 'type' => 'string' },
- 'lastUpdated' => { 'type' => 'string' },
+ 'lastUpdated' => iso8601_type,
'url' => { 'type' => 'string' },
'state' => state_type,
'issueKeys' => issue_keys_type,
@@ -82,7 +82,7 @@ module Atlassian
'description' => { 'type' => 'string' },
'label' => { 'type' => 'string' },
'url' => { 'type' => 'string' },
- 'lastUpdated' => { 'type' => 'string' },
+ 'lastUpdated' => iso8601_type,
'state' => state_type,
'pipeline' => pipeline_type,
'environment' => environment_type,
@@ -91,6 +91,93 @@ module Atlassian
}
end
+ def feature_flag_info
+ {
+ 'type' => 'object',
+ 'additionalProperties' => false,
+ 'required' => %w(
+ updateSequenceId id key issueKeys summary details
+ ),
+ 'properties' => {
+ 'id' => { 'type' => 'string' },
+ 'key' => { 'type' => 'string' },
+ 'displayName' => { 'type' => 'string' },
+ 'issueKeys' => issue_keys_type,
+ 'summary' => summary_type,
+ 'details' => details_type,
+ 'updateSequenceId' => { 'type' => 'integer' },
+ 'schemaVersion' => schema_version_type
+ }
+ }
+ end
+
+ def details_type
+ {
+ 'type' => 'array',
+ 'items' => combine(summary_type, {
+ 'required' => ['environment'],
+ 'properties' => {
+ 'environment' => {
+ 'type' => 'object',
+ 'additionalProperties' => false,
+ 'required' => %w(name),
+ 'properties' => {
+ 'name' => { 'type' => 'string' },
+ 'type' => {
+ 'type' => 'string',
+ 'pattern' => '^(development|testing|staging|production)$'
+ }
+ }
+ }
+ }
+ })
+ }
+ end
+
+ def combine(map_a, map_b)
+ map_a.merge(map_b) do |k, a, b|
+ a.respond_to?(:merge) ? a.merge(b) : a + b
+ end
+ end
+
+ def summary_type
+ {
+ 'type' => 'object',
+ 'additionalProperties' => false,
+ 'required' => %w(url status lastUpdated),
+ 'properties' => {
+ 'lastUpdated' => iso8601_type,
+ 'url' => { 'type' => 'string' },
+ 'status' => feature_status_type
+ }
+ }
+ end
+
+ def feature_status_type
+ {
+ 'type' => 'object',
+ 'additionalProperties' => false,
+ 'required' => %w(enabled),
+ 'properties' => {
+ 'enabled' => { 'type' => 'boolean' },
+ 'defaultValue' => { 'type' => 'string' },
+ 'rollout' => rollout_type
+ }
+ }
+ end
+
+ def rollout_type
+ {
+ 'type' => 'object',
+ 'additionalProperties' => false,
+ 'properties' => {
+ 'percentage' => { 'type' => 'number' },
+ 'text' => { 'type' => 'string' },
+ 'rules' => { 'type' => 'number' }
+ }
+ }
+ end
+
def environment_type
{
'type' => 'object',
@@ -163,9 +250,21 @@ module Atlassian
payload('builds', build_info)
end
+ def ff_info_payload
+ pl = payload('flags', feature_flag_info)
+ pl['properties']['properties'] = {
+ 'type' => 'object',
+ 'additionalProperties' => { 'type' => 'string' },
+ 'maxProperties' => 5,
+ 'propertyNames' => { 'pattern' => '^[^_][^:]+$' }
+ }
+ pl
+ end
+
def payload(key, schema)
{
'type' => 'object',
+ 'additionalProperties' => false,
'required' => ['providerMetadata', key],
'properties' => {
'providerMetadata' => provider_metadata,
@@ -181,6 +280,13 @@ module Atlassian
'properties' => { 'product' => { 'type' => 'string' } }
}
end
+
+ def iso8601_type
+ {
+ 'type' => 'string',
+ 'pattern' => '^-?([1-9][0-9]*)?[0-9]{4}-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$'
+ }
+ end
end
end
end
diff --git a/spec/support/gitlab_stubs/gitlab_ci_includes.yml b/spec/support/gitlab_stubs/gitlab_ci_includes.yml
index e74773ce23e..1029fa1ea86 100644
--- a/spec/support/gitlab_stubs/gitlab_ci_includes.yml
+++ b/spec/support/gitlab_stubs/gitlab_ci_includes.yml
@@ -1,19 +1,45 @@
+before_script:
+ - bundle install
+ - bundle exec rake db:create
+
rspec 0 1:
stage: build
script: 'rake spec'
needs: []
+ tags:
+ - ruby
+ - postgres
+ only:
+ - branches
+ - master
rspec 0 2:
stage: build
+ allow_failure: true
script: 'rake spec'
+ when: on_failure
needs: []
spinach:
stage: build
script: 'rake spinach'
needs: []
+ except:
+ - tags
+deploy_job:
+ stage: deploy
+ script:
+ - echo 'done'
+ environment:
+ name: production
docker:
stage: test
script: 'curl http://dockerhub/URL'
needs: [spinach, rspec 0 1]
+ when: manual
+ except:
+ - branches
+
+after_script:
+ - echo 'run this after'
diff --git a/spec/support/matchers/schema_matcher.rb b/spec/support/matchers/schema_matcher.rb
index a7696a511bd..f0e7a52c51e 100644
--- a/spec/support/matchers/schema_matcher.rb
+++ b/spec/support/matchers/schema_matcher.rb
@@ -37,7 +37,13 @@ RSpec::Matchers.define :match_schema do |schema, dir: nil, **options|
end
failure_message do |response|
- "didn't match the schema defined by #{SchemaPath.expand(schema, dir)}" \
+ "didn't match the schema defined by #{schema_name(schema, dir)}" \
" The validation errors were:\n#{@errors.join("\n")}"
end
+
+ def schema_name(schema, dir)
+ return 'provided schema' unless schema.is_a?(String)
+
+ SchemaPath.expand(schema, dir)
+ end
end
diff --git a/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb b/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb
new file mode 100644
index 00000000000..035f4ebdd3c
--- /dev/null
+++ b/spec/workers/jira_connect/sync_feature_flags_worker_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::JiraConnect::SyncFeatureFlagsWorker do
+ include AfterNextHelpers
+ include ServicesHelper
+
+ describe '#perform' do
+ let_it_be(:feature_flag) { create(:operations_feature_flag) }
+
+ let(:sequence_id) { Random.random_number(1..10_000) }
+ let(:feature_flag_id) { feature_flag.id }
+
+ subject { described_class.new.perform(feature_flag_id, sequence_id) }
+
+ context 'when object exists' do
+ it 'calls the Jira sync service' do
+ expect_next(::JiraConnect::SyncService, feature_flag.project)
+ .to receive(:execute).with(feature_flags: contain_exactly(feature_flag), update_sequence_id: sequence_id)
+
+ subject
+ end
+ end
+
+ context 'when object does not exist' do
+ let(:feature_flag_id) { non_existing_record_id }
+
+ it 'does not call the sync service' do
+ expect_next(::JiraConnect::SyncService).not_to receive(:execute)
+
+ subject
+ end
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(jira_sync_feature_flags: false)
+ end
+
+ it 'does not call the sync service' do
+ expect_next(::JiraConnect::SyncService).not_to receive(:execute)
+
+ subject
+ end
+ end
+
+ context 'when the feature flag is enabled for this project' do
+ before do
+ stub_feature_flags(jira_sync_feature_flags: feature_flag.project)
+ end
+
+ it 'calls the sync service' do
+ expect_next(::JiraConnect::SyncService).to receive(:execute)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 21d2280c7da..4a4a536383e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -845,7 +845,7 @@
resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.5.tgz#5f6bfe6baaef360daa9b038fa78798d7a6a916b4"
integrity sha512-282Dn3SPVsUHVDhMsXgfnv+Rzog0uxecjttxGRQvxh25es1+xvkGQFsvJfkSKJ3X1kHVkSjKf+Tt5Rra+Jhp9g==
-"@gitlab/eslint-plugin@^6.0.0":
+"@gitlab/eslint-plugin@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-6.0.0.tgz#deb18f63808af1cb1cc117a92558f07edb1e2256"
integrity sha512-3TihEG0EzbGtc6wxZLANZN1ge2tnAv0qU8w6smUACmPhqFj0/DrCq9V6QKPqAHk/Yn3hrfGk5nznAzzuMEgwDQ==