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:
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml6
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml25
-rw-r--r--.gitlab/merge_request_templates/Deprecations.md15
-rw-r--r--.gitlab/merge_request_templates/Removals.md28
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql7
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql7
-rw-r--r--app/assets/javascripts/integrations/constants.js2
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue19
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue17
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue21
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue19
-rw-r--r--app/assets/javascripts/integrations/edit/event_hub.js3
-rw-r--r--app/assets/javascripts/integrations/edit/store/actions.js3
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/actions_cell.vue20
-rw-r--r--app/models/application_record.rb1
-rw-r--r--app/models/concerns/cross_database_modification.rb87
-rw-r--r--app/services/users/destroy_service.rb5
-rw-r--r--config/feature_flags/development/track_gitlab_schema_in_current_transaction.yml8
-rw-r--r--db/migrate/20220105082217_add_verification_token_to_external_ae_destinations.rb13
-rw-r--r--db/migrate/20220117082611_add_text_limit_to_exad_verification_tokens.rb13
-rw-r--r--db/migrate/20220119094023_add_unique_index_to_aed_verification_token.rb15
-rw-r--r--db/post_migrate/20220119094503_populate_audit_event_streaming_verification_token.rb19
-rw-r--r--db/schema_migrations/202201050822171
-rw-r--r--db/schema_migrations/202201170826111
-rw-r--r--db/schema_migrations/202201190940231
-rw-r--r--db/schema_migrations/202201190945031
-rw-r--r--db/structure.sql6
-rw-r--r--doc/administration/audit_event_streaming.md14
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/ci/examples/authenticating-with-hashicorp-vault/index.md4
-rw-r--r--doc/ci/secrets/index.md4
-rw-r--r--doc/install/installation.md12
-rw-r--r--doc/user/permissions.md104
-rw-r--r--doc/user/project/code_owners.md2
-rw-r--r--lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb2
-rw-r--r--lib/gitlab/import_export/project/object_builder.rb6
-rw-r--r--spec/features/boards/board_filters_spec.rb11
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js58
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js18
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js25
-rw-r--r--spec/frontend/integrations/edit/mock_data.js9
-rw-r--r--spec/frontend/jobs/components/table/cells/actions_cell_spec.js31
-rw-r--r--spec/frontend/jobs/mock_data.js59
-rw-r--r--spec/graphql/resolvers/package_pipelines_resolver_spec.rb5
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb41
-rw-r--r--spec/migrations/populate_audit_event_streaming_verification_token_spec.rb22
-rw-r--r--spec/models/concerns/cross_database_modification_spec.rb89
-rw-r--r--spec/requests/api/graphql/packages/package_spec.rb5
49 files changed, 630 insertions, 259 deletions
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 76f5d8f3eee..4b9c91e4e94 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -339,12 +339,6 @@ rspec fast_spec_helper:
# Load fast_spec_helper as well just in case there are no specs available.
- bin/rspec --dry-run spec/fast_spec_helper.rb $fast_spec_helper_specs
-rspec fast_spec_helper minimal:
- extends:
- - rspec fast_spec_helper
- - .minimal-rspec-tests
- - .rails:rules:ee-and-foss-fast_spec_helper:minimal
-
db:rollback:
extends: .db-job-base
script:
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 973d022c197..58f5e716bc5 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -980,33 +980,8 @@
changes: *core-backend-patterns
- <<: *if-merge-request
changes: *ci-patterns
- - <<: *if-automated-merge-request
- changes: ["config/**/*"]
- - <<: *if-security-merge-request
- changes: ["config/**/*"]
- - <<: *if-merge-request-not-approved
- when: never
- changes: ["config/**/*"]
-.rails:rules:ee-and-foss-fast_spec_helper:minimal:
- rules:
- - <<: *if-merge-request-approved
- when: never
- - <<: *if-automated-merge-request
- when: never
- - <<: *if-security-merge-request
- when: never
- - <<: *if-merge-request-labels-run-all-rspec
- when: never
- - <<: *if-merge-request
- changes: *core-backend-patterns
- when: never
- - <<: *if-merge-request
- changes: *ci-patterns
- when: never
- - <<: *if-merge-request
- changes: ["config/**/*"]
-
.rails:rules:code-backstage-qa:
rules:
- changes: *code-backstage-qa-patterns
diff --git a/.gitlab/merge_request_templates/Deprecations.md b/.gitlab/merge_request_templates/Deprecations.md
index 38d956a8e90..b06be633c67 100644
--- a/.gitlab/merge_request_templates/Deprecations.md
+++ b/.gitlab/merge_request_templates/Deprecations.md
@@ -1,12 +1,18 @@
<!-- Set the correct label and milestone using autocomplete for guidance. Please @mention only the DRI(s) for each stage or group rather than an entire department. -->
-/label ~"release post" ~"release post item" ~"Technical Writing" ~"devops::" ~"group::" ~"release post item::deprecation"
+/label ~"release post" ~"release post item" ~"Technical Writing" ~devops:: ~group:: ~"release post item::deprecation"
/milestone %
/assign `@EM/PM` (choose the DRI; remove backticks here, and below)
**Be sure to link this MR to the relevant deprecation issue(s).**
-If the MR does not have a deprecation issue, hit pause and review [this handbook documentation](https://about.gitlab.com/handbook/product/gitlab-the-product/#process-for-deprecating-and-removing-a-feature) and connect with the Product Manager DRI.
+- Deprecation Issue:
+- MR that deprecates the feature (optional):
+
+If there is no relevant removal or deprecation issue, hit pause and:
+
+- Review the [process for deprecating and removing features](https://about.gitlab.com/handbook/product/gitlab-the-product/#process-for-deprecating-and-removing-a-feature).
+- Connect with the Product Manager DRI.
Deprecation announcements can and should be created and merged into Docs at any time, to optimize user awareness and planning. We encourage confirmed deprecations to be merged as soon as the required reviews are complete, even if weeks ahead of the target milestone's release post. For the announcement to be included in a specific release post and that release's documentation packages, this MR must be reviewed/merged per the due dates below:
@@ -28,11 +34,6 @@ Please review the [guidelines for deprecations](https://about.gitlab.com/handboo
as well as the process for [creating a deprecation entry](https://about.gitlab.com/handbook/marketing/blog/release-posts/#creating-a-deprecation-entry).
They are frequently updated, and everyone should make sure they are aware of the current standards (PM, PMM, EM, and TW).
-## Links
-
-- Deprecation Issue:
-- Deprecation MR (optional):
-
## EM/PM release post item checklist
- [ ] Set yourself as the Assignee, meaning you are the DRI.
diff --git a/.gitlab/merge_request_templates/Removals.md b/.gitlab/merge_request_templates/Removals.md
index 398714826b9..341ea5e831b 100644
--- a/.gitlab/merge_request_templates/Removals.md
+++ b/.gitlab/merge_request_templates/Removals.md
@@ -1,13 +1,22 @@
<!-- Set the correct label and milestone using autocomplete for guidance. Please @mention only the DRI(s) for each stage or group rather than an entire department. -->
-**Be sure to link this MR to the relevant deprecation issue(s).**
+/label ~"release post" ~"release post item" ~"Technical Writing" ~devops:: ~group:: ~"release post item::removal"
+/milestone %
+/assign `@EM/PM` (choose the DRI; remove backticks here, and below)
+
+**Be sure to link this MR to the relevant issue(s).**
+
+- Removal Issue (or earlier deprecation issue):
+- MR that removes the feature (optional):
+
+If there is no relevant removal or deprecation issue, hit pause and:
-If the MR does not have a deprecation issue, hit pause and:
+- Review the [process for deprecating and removing features](https://about.gitlab.com/handbook/product/gitlab-the-product/#process-for-deprecating-and-removing-a-feature).
+- Connect with the Product Manager DRI.
-- Review [this handbook documentation](https://about.gitlab.com/handbook/product/gitlab-the-product/#process-for-deprecating-and-removing-a-feature).
-- Connect with the Product Manager DRI.
+Removals must be [announced as deprecations](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations) at least 2 milestones in advance of the planned removal date.
-Removals must be [announced as Deprecations](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations) at least 2 milestones in advance of the planned removal date. Removals can only be removed in a XX.0 major release if it creates a [breaking change](https://about.gitlab.com/handbook/product/gitlab-the-product/#breaking-changes-deprecations-and-removing-features).
+If the removal creates a [breaking change](https://about.gitlab.com/handbook/product/gitlab-the-product/#breaking-changes-deprecations-and-removing-features), it can only be removed in a major "XX.0" release.
**By the 10th**: Assign this MR to these team members as reviewers, and for approval:
@@ -27,11 +36,6 @@ Removals must be [announced as Deprecations](https://about.gitlab.com/handbook/m
Please review the [guidelines for removals](https://about.gitlab.com/handbook/marketing/blog/release-posts/#removals).
-## Links
-
-- Removal Issue:
-- Removal MR (optional):
-
## EM/PM release post item checklist
- [ ] Set yourself as the Assignee, meaning you are the DRI.
@@ -97,7 +101,3 @@ must be updated before this MR is merged:
1. Set the MR to merge when the pipeline succeeds (or merge if the pipeline is already complete).
If you have trouble running the rake task, check the [troubleshooting steps](https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecation-rake-task-troubleshooting).
-
-/label ~"release post" ~"release post item" ~"Technical Writing" ~devops:: ~group:: ~"release post item::removal"
-/milestone %
-/assign `@EM/PM` (choose the DRI; remove backticks here, and below)
diff --git a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
index 6fe8bb799d6..9e6c26063e9 100644
--- a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
@@ -1,7 +1,12 @@
query GroupBoardMilestones($fullPath: ID!, $searchTerm: String, $state: MilestoneStateEnum) {
group(fullPath: $fullPath) {
id
- milestones(includeAncestors: true, searchTitle: $searchTerm, state: $state) {
+ milestones(
+ includeAncestors: true
+ searchTitle: $searchTerm
+ state: $state
+ sort: EXPIRED_LAST_DUE_DATE_ASC
+ ) {
nodes {
id
title
diff --git a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
index d917c7e809d..02aa08f90ef 100644
--- a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
@@ -1,7 +1,12 @@
query ProjectBoardMilestones($fullPath: ID!, $searchTerm: String, $state: MilestoneStateEnum) {
project(fullPath: $fullPath) {
id
- milestones(searchTitle: $searchTerm, includeAncestors: true, state: $state) {
+ milestones(
+ searchTitle: $searchTerm
+ includeAncestors: true
+ state: $state
+ sort: EXPIRED_LAST_DUE_DATE_ASC
+ ) {
nodes {
id
title
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index b90658fb13c..b26f6bdd370 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -1,7 +1,5 @@
import { s__, __ } from '~/locale';
-export const VALIDATE_INTEGRATION_FORM_EVENT = 'validateIntegrationForm';
-
export const integrationLevels = {
GROUP: 'group',
INSTANCE: 'instance',
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index 4b0579a5beb..b4ceec22822 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -9,8 +9,6 @@ import {
} from '@gitlab/ui';
import { capitalize, lowerCase, isEmpty } from 'lodash';
import { mapGetters } from 'vuex';
-import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
-import eventHub from '../event_hub';
export default {
name: 'DynamicField',
@@ -70,11 +68,15 @@ export default {
required: false,
default: null,
},
+ isValidated: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
model: this.value,
- validated: false,
};
},
computed: {
@@ -123,22 +125,13 @@ export default {
};
},
valid() {
- return !this.required || !isEmpty(this.model) || this.isNonEmptyPassword || !this.validated;
+ return !this.required || !isEmpty(this.model) || this.isNonEmptyPassword || !this.isValidated;
},
},
created() {
if (this.isNonEmptyPassword) {
this.model = null;
}
- eventHub.$on(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
- },
- beforeDestroy() {
- eventHub.$off(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
- },
- methods: {
- validateForm() {
- this.validated = true;
- },
},
helpHtmlConfig: {
ADD_ATTR: ['target'], // allow external links, can be removed after https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1427 is implemented
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index c3cc35adfa5..dcdb38d94c8 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -5,7 +5,6 @@ import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
- VALIDATE_INTEGRATION_FORM_EVENT,
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
@@ -14,7 +13,6 @@ import {
} from '~/integrations/constants';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import csrf from '~/lib/utils/csrf';
-import eventHub from '../event_hub';
import { testIntegrationSettings } from '../api';
import ActiveCheckbox from './active_checkbox.vue';
import ConfirmationModal from './confirmation_modal.vue';
@@ -57,6 +55,7 @@ export default {
isTesting: false,
isSaving: false,
isResetting: false,
+ isValidated: false,
};
},
computed: {
@@ -107,13 +106,16 @@ export default {
: document.querySelector(INTEGRATION_FORM_SELECTOR);
},
methods: {
- ...mapActions(['setOverride', 'fetchResetIntegration', 'requestJiraIssueTypes']),
+ ...mapActions(['setOverride', 'requestJiraIssueTypes']),
+ setIsValidated() {
+ this.isValidated = true;
+ },
onSaveClick() {
this.isSaving = true;
if (this.integrationActive && !this.form.checkValidity()) {
this.isSaving = false;
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+ this.setIsValidated();
return;
}
@@ -123,14 +125,14 @@ export default {
this.isTesting = true;
if (!this.form.checkValidity()) {
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+ this.setIsValidated();
return;
}
testIntegrationSettings(this.propsSource.testPath, this.getFormData())
.then(({ data: { error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } }) => {
if (error) {
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
+ this.setIsValidated();
this.$toast.show(message);
return;
}
@@ -227,6 +229,7 @@ export default {
v-if="isJira"
:key="`${currentKey}-jira-trigger-fields`"
v-bind="propsSource.triggerFieldsProps"
+ :is-validated="isValidated"
/>
<trigger-fields
v-else-if="propsSource.triggerEvents.length"
@@ -238,11 +241,13 @@ export default {
v-for="field in propsSource.fields"
:key="`${currentKey}-${field.name}`"
v-bind="field"
+ :is-validated="isValidated"
/>
<jira-issues-fields
v-if="isJira && !isInstanceOrGroupLevel"
:key="`${currentKey}-jira-issues-fields`"
v-bind="propsSource.jiraIssuesProps"
+ :is-validated="isValidated"
@request-jira-issue-types="onRequestJiraIssueTypes"
/>
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index 99498501f6c..7f2f7620a86 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -1,9 +1,7 @@
<script>
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
import { mapGetters } from 'vuex';
-import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import { s__, __ } from '~/locale';
-import eventHub from '../event_hub';
import JiraUpgradeCta from './jira_upgrade_cta.vue';
export default {
@@ -64,29 +62,22 @@ export default {
required: false,
default: '',
},
+ isValidated: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
enableJiraIssues: this.initialEnableJiraIssues,
projectKey: this.initialProjectKey,
- validated: false,
};
},
computed: {
...mapGetters(['isInheriting']),
validProjectKey() {
- return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
- },
- },
- created() {
- eventHub.$on(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
- },
- beforeDestroy() {
- eventHub.$off(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
- },
- methods: {
- validateForm() {
- this.validated = true;
+ return !this.enableJiraIssues || Boolean(this.projectKey) || !this.isValidated;
},
},
i18n: {
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
index 249a3e105b1..df5946b814a 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
@@ -9,9 +9,7 @@ import {
} from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import { s__ } from '~/locale';
-import eventHub from '../event_hub';
const commentDetailOptions = [
{
@@ -92,10 +90,14 @@ export default {
required: false,
default: '',
},
+ isValidated: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
- validated: false,
triggerCommit: this.initialTriggerCommit,
triggerMergeRequest: this.initialTriggerMergeRequest,
enableComments: this.initialEnableComments,
@@ -115,19 +117,10 @@ export default {
return this.triggerCommit || this.triggerMergeRequest;
},
validIssueTransitionId() {
- return !this.validated || Boolean(this.jiraIssueTransitionId);
+ return !this.isValidated || Boolean(this.jiraIssueTransitionId);
},
},
- created() {
- eventHub.$on(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
- },
- beforeDestroy() {
- eventHub.$off(VALIDATE_INTEGRATION_FORM_EVENT, this.validateForm);
- },
methods: {
- validateForm() {
- this.validated = true;
- },
showCustomIssueTransitions(currentOption) {
return (
this.jiraIssueTransitionAutomatic === ISSUE_TRANSITION_CUSTOM &&
diff --git a/app/assets/javascripts/integrations/edit/event_hub.js b/app/assets/javascripts/integrations/edit/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/integrations/edit/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js
index 1398b710d1d..d31d3eb9d82 100644
--- a/app/assets/javascripts/integrations/edit/store/actions.js
+++ b/app/assets/javascripts/integrations/edit/store/actions.js
@@ -1,10 +1,8 @@
import {
- VALIDATE_INTEGRATION_FORM_EVENT,
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
} from '~/integrations/constants';
import { testIntegrationSettings } from '../api';
-import eventHub from '../event_hub';
import * as types from './mutation_types';
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
@@ -19,7 +17,6 @@ export const requestJiraIssueTypes = ({ commit, dispatch, getters }, formData) =
data: { issuetypes, error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE },
}) => {
if (error || !issuetypes?.length) {
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
throw new Error(message);
}
diff --git a/app/assets/javascripts/integrations/edit/store/mutation_types.js b/app/assets/javascripts/integrations/edit/store/mutation_types.js
index ddf6bef7554..eb74b0b1c73 100644
--- a/app/assets/javascripts/integrations/edit/store/mutation_types.js
+++ b/app/assets/javascripts/integrations/edit/store/mutation_types.js
@@ -1,9 +1,5 @@
export const SET_OVERRIDE = 'SET_OVERRIDE';
-export const SET_IS_RESETTING = 'SET_IS_RESETTING';
export const SET_IS_LOADING_JIRA_ISSUE_TYPES = 'SET_IS_LOADING_JIRA_ISSUE_TYPES';
export const SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE = 'SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE';
export const SET_JIRA_ISSUE_TYPES = 'SET_JIRA_ISSUE_TYPES';
-
-export const REQUEST_RESET_INTEGRATION = 'REQUEST_RESET_INTEGRATION';
-export const RECEIVE_RESET_INTEGRATION_ERROR = 'RECEIVE_RESET_INTEGRATION_ERROR';
diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
index 7dfa963a857..753a15871ab 100644
--- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
+++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
@@ -58,6 +58,14 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ retryBtnDisabled: false,
+ cancelBtnDisabled: false,
+ playManualBtnDisabled: false,
+ unscheduleBtnDisabled: false,
+ };
+ },
computed: {
hasArtifacts() {
return this.job.artifacts.nodes.find((artifact) => artifact.fileType === FILE_TYPE_ARCHIVE);
@@ -132,15 +140,23 @@ export default {
});
},
cancelJob() {
+ this.cancelBtnDisabled = true;
+
this.postJobAction(this.$options.jobCancel, cancelJobMutation);
},
retryJob() {
+ this.retryBtnDisabled = true;
+
this.postJobAction(this.$options.jobRetry, retryJobMutation);
},
playJob() {
+ this.playManualBtnDisabled = true;
+
this.postJobAction(this.$options.jobPlay, playJobMutation);
},
unscheduleJob() {
+ this.unscheduleBtnDisabled = true;
+
this.postJobAction(this.$options.jobUnschedule, unscheduleJobMutation);
},
},
@@ -155,6 +171,7 @@ export default {
data-testid="cancel-button"
icon="cancel"
:title="$options.CANCEL"
+ :disabled="cancelBtnDisabled"
@click="cancelJob()"
/>
<template v-else-if="isScheduled">
@@ -179,6 +196,7 @@ export default {
<gl-button
icon="time-out"
:title="$options.ACTIONS_UNSCHEDULE"
+ :disabled="unscheduleBtnDisabled"
data-testid="unschedule"
@click="unscheduleJob()"
/>
@@ -189,6 +207,7 @@ export default {
v-if="manualJobPlayable"
icon="play"
:title="$options.ACTIONS_PLAY"
+ :disabled="playManualBtnDisabled"
data-testid="play"
@click="playJob()"
/>
@@ -197,6 +216,7 @@ export default {
icon="repeat"
:title="$options.ACTIONS_RETRY"
:method="currentJobMethod"
+ :disabled="retryBtnDisabled"
data-testid="retry"
@click="retryJob()"
/>
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index b64e6c59817..06ff18ca409 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -4,6 +4,7 @@ class ApplicationRecord < ActiveRecord::Base
include DatabaseReflection
include Transactions
include LegacyBulkInsert
+ include CrossDatabaseModification
self.abstract_class = true
diff --git a/app/models/concerns/cross_database_modification.rb b/app/models/concerns/cross_database_modification.rb
new file mode 100644
index 00000000000..9efff6d580a
--- /dev/null
+++ b/app/models/concerns/cross_database_modification.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module CrossDatabaseModification
+ extend ActiveSupport::Concern
+
+ class TransactionStackTrackRecord
+ def initialize(subject, gitlab_schema)
+ @subject = subject
+ @gitlab_schema = gitlab_schema
+ @subject.gitlab_transactions_stack.push(gitlab_schema)
+ end
+
+ def done!
+ unless @done
+ @done = true
+ @subject.gitlab_transactions_stack.pop
+ end
+
+ true
+ end
+
+ def trigger_transactional_callbacks?
+ false
+ end
+
+ def before_committed!
+ end
+
+ def rolledback!(force_restore_state: false, should_run_callbacks: true)
+ done!
+ end
+
+ def committed!(should_run_callbacks: true)
+ done!
+ end
+ end
+
+ included do
+ private_class_method :gitlab_schema
+ end
+
+ class_methods do
+ def gitlab_transactions_stack
+ Thread.current[:gitlab_transactions_stack] ||= []
+ end
+
+ def transaction(**options, &block)
+ if track_gitlab_schema_in_current_transaction?
+ super(**options) do
+ # Hook into current transaction to ensure that once
+ # the `COMMIT` is executed the `gitlab_transactions_stack`
+ # will be allowing to execute `after_commit_queue`
+ record = TransactionStackTrackRecord.new(self, gitlab_schema)
+
+ begin
+ connection.current_transaction.add_record(record)
+
+ yield
+ ensure
+ record.done!
+ end
+ end
+ else
+ super(**options, &block)
+ end
+ end
+
+ def track_gitlab_schema_in_current_transaction?
+ return false unless Feature::FlipperFeature.table_exists?
+
+ Feature.enabled?(:track_gitlab_schema_in_current_transaction, default_enabled: :yaml)
+ rescue ActiveRecord::NoDatabaseError, PG::ConnectionBad
+ false
+ end
+
+ def gitlab_schema
+ case self.name
+ when 'ActiveRecord::Base', 'ApplicationRecord'
+ :gitlab_main
+ when 'Ci::ApplicationRecord'
+ :gitlab_ci
+ else
+ Gitlab::Database::GitlabSchema.table_schema(table_name) if table_name
+ end
+ end
+ end
+end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 1634cc017ae..4ec875098fa 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -65,10 +65,7 @@ module Users
user.destroy_dependent_associations_in_batches(exclude: [:snippets])
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
- user_data = nil
- ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/340260') do
- user_data = user.destroy
- end
+ user_data = user.destroy
namespace.destroy
user_data
diff --git a/config/feature_flags/development/track_gitlab_schema_in_current_transaction.yml b/config/feature_flags/development/track_gitlab_schema_in_current_transaction.yml
new file mode 100644
index 00000000000..a4685732f27
--- /dev/null
+++ b/config/feature_flags/development/track_gitlab_schema_in_current_transaction.yml
@@ -0,0 +1,8 @@
+---
+name: track_gitlab_schema_in_current_transaction
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76717
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349944
+milestone: '14.8'
+type: development
+group: group::sharding
+default_enabled: false
diff --git a/db/migrate/20220105082217_add_verification_token_to_external_ae_destinations.rb b/db/migrate/20220105082217_add_verification_token_to_external_ae_destinations.rb
new file mode 100644
index 00000000000..046a628b8ae
--- /dev/null
+++ b/db/migrate/20220105082217_add_verification_token_to_external_ae_destinations.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddVerificationTokenToExternalAeDestinations < Gitlab::Database::Migration[1.0]
+ def up
+ # rubocop:disable Migration/AddLimitToTextColumns
+ add_column :audit_events_external_audit_event_destinations, :verification_token, :text
+ # rubocop:enable Migration/AddLimitToTextColumns
+ end
+
+ def down
+ remove_column :audit_events_external_audit_event_destinations, :verification_token
+ end
+end
diff --git a/db/migrate/20220117082611_add_text_limit_to_exad_verification_tokens.rb b/db/migrate/20220117082611_add_text_limit_to_exad_verification_tokens.rb
new file mode 100644
index 00000000000..9978e87a1e3
--- /dev/null
+++ b/db/migrate/20220117082611_add_text_limit_to_exad_verification_tokens.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class AddTextLimitToExadVerificationTokens < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ add_text_limit :audit_events_external_audit_event_destinations, :verification_token, 24
+ end
+
+ def down
+ remove_text_limit :audit_events_external_audit_event_destinations, :verification_token
+ end
+end
diff --git a/db/migrate/20220119094023_add_unique_index_to_aed_verification_token.rb b/db/migrate/20220119094023_add_unique_index_to_aed_verification_token.rb
new file mode 100644
index 00000000000..bd40fe2203e
--- /dev/null
+++ b/db/migrate/20220119094023_add_unique_index_to_aed_verification_token.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class AddUniqueIndexToAedVerificationToken < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_audit_events_external_audit_on_verification_token'
+
+ def up
+ add_concurrent_index :audit_events_external_audit_event_destinations, :verification_token, unique: true, name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :audit_events_external_audit_event_destinations, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20220119094503_populate_audit_event_streaming_verification_token.rb b/db/post_migrate/20220119094503_populate_audit_event_streaming_verification_token.rb
new file mode 100644
index 00000000000..482f873739a
--- /dev/null
+++ b/db/post_migrate/20220119094503_populate_audit_event_streaming_verification_token.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class PopulateAuditEventStreamingVerificationToken < Gitlab::Database::Migration[1.0]
+ class ExternalAuditEventDestination < ActiveRecord::Base
+ self.table_name = 'audit_events_external_audit_event_destinations'
+
+ def regenerate_verification_token
+ update!(verification_token: SecureRandom.base58(24))
+ end
+ end
+
+ def up
+ ExternalAuditEventDestination.all.each { |destination| destination.regenerate_verification_token }
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20220105082217 b/db/schema_migrations/20220105082217
new file mode 100644
index 00000000000..a6232406be6
--- /dev/null
+++ b/db/schema_migrations/20220105082217
@@ -0,0 +1 @@
+448481ec9f7dd58d267e3660a49161c0e14baca35e640c59b27f2ebc4367b62a \ No newline at end of file
diff --git a/db/schema_migrations/20220117082611 b/db/schema_migrations/20220117082611
new file mode 100644
index 00000000000..9dead593ffb
--- /dev/null
+++ b/db/schema_migrations/20220117082611
@@ -0,0 +1 @@
+28df9a8b5bf73bc33275cfe47f260788fa3263680a97128e086fd1698ccac1d8 \ No newline at end of file
diff --git a/db/schema_migrations/20220119094023 b/db/schema_migrations/20220119094023
new file mode 100644
index 00000000000..c2bf5ce88d6
--- /dev/null
+++ b/db/schema_migrations/20220119094023
@@ -0,0 +1 @@
+4eddd356d87ce8fc8168dabe678211239e8d4051804d51d3bdce8cc137fa5a0d \ No newline at end of file
diff --git a/db/schema_migrations/20220119094503 b/db/schema_migrations/20220119094503
new file mode 100644
index 00000000000..840a4d82593
--- /dev/null
+++ b/db/schema_migrations/20220119094503
@@ -0,0 +1 @@
+1048b3a9744f212297c0a3aba176556e92e85f199ac861eb3ee4183eff002860 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 00ec4445f09..493ff4300a1 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -10775,7 +10775,9 @@ CREATE TABLE audit_events_external_audit_event_destinations (
destination_url text NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
- CONSTRAINT check_2feafb9daf CHECK ((char_length(destination_url) <= 255))
+ verification_token text,
+ CONSTRAINT check_2feafb9daf CHECK ((char_length(destination_url) <= 255)),
+ CONSTRAINT check_8ec80a7d06 CHECK ((char_length(verification_token) <= 24))
);
CREATE SEQUENCE audit_events_external_audit_event_destinations_id_seq
@@ -25355,6 +25357,8 @@ CREATE INDEX index_approvers_on_user_id ON approvers USING btree (user_id);
CREATE UNIQUE INDEX index_atlassian_identities_on_extern_uid ON atlassian_identities USING btree (extern_uid);
+CREATE UNIQUE INDEX index_audit_events_external_audit_on_verification_token ON audit_events_external_audit_event_destinations USING btree (verification_token);
+
CREATE INDEX index_authentication_events_on_provider ON authentication_events USING btree (provider);
CREATE INDEX index_authentication_events_on_provider_user_id_created_at ON authentication_events USING btree (provider, user_id, created_at) WHERE (result = 1);
diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md
index eac54416924..59918e5660a 100644
--- a/doc/administration/audit_event_streaming.md
+++ b/doc/administration/audit_event_streaming.md
@@ -13,7 +13,7 @@ FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature per group, ask an administrator to [disable the feature flag](../administration/feature_flags.md) named `ff_external_audit_events_namespace`. On GitLab.com, this feature is available.
Event streaming allows owners of top-level groups to set an HTTP endpoint to receive **all** audit events about the group, and its
-subgroups and projects.
+subgroups and projects as structured JSON.
Top-level group owners can manage their audit logs in third-party systems such as Splunk, using the Splunk
[HTTP Event Collector](https://docs.splunk.com/Documentation/Splunk/8.2.2/Data/UsetheHTTPEventCollector). Any service that can receive
@@ -37,6 +37,7 @@ mutation {
externalAuditEventDestination {
destinationUrl
group {
+ verificationToken
name
}
}
@@ -60,6 +61,7 @@ query {
externalAuditEventDestinations {
nodes {
destinationUrl
+ verificationToken
id
}
}
@@ -68,3 +70,13 @@ query {
```
If the resulting list is empty, then audit event streaming is not enabled for that group.
+
+## Verify event authenticity
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/345424) in GitLab 14.8.
+
+Each streaming destination has a unique verification token (`verificationToken`) that can be used to verify the authenticity of the event. This
+token is generated when the event destination is created and cannot be changed.
+
+Each streamed event contains a random alphanumeric identifier for the `X-Gitlab-Event-Streaming-Token` HTTP header that can be verified against
+the destination's value when [listing streaming destinations](#list-currently-enabled-streaming-destinations).
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 818192f5a2a..bb069e2ad66 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -10531,6 +10531,7 @@ Represents an external resource to send audit events to.
| <a id="externalauditeventdestinationdestinationurl"></a>`destinationUrl` | [`String!`](#string) | External destination to send audit events to. |
| <a id="externalauditeventdestinationgroup"></a>`group` | [`Group!`](#group) | Group the destination belongs to. |
| <a id="externalauditeventdestinationid"></a>`id` | [`ID!`](#id) | ID of the destination. |
+| <a id="externalauditeventdestinationverificationtoken"></a>`verificationToken` | [`String!`](#string) | Verification token to validate source of event. |
### `ExternalIssue`
diff --git a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
index aed45951239..edc58684057 100644
--- a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
+++ b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md
@@ -1,6 +1,6 @@
---
-stage: Release
-group: Release
+stage: Verify
+group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: tutorial
---
diff --git a/doc/ci/secrets/index.md b/doc/ci/secrets/index.md
index c0a763c80f0..ea0c0d9cc84 100644
--- a/doc/ci/secrets/index.md
+++ b/doc/ci/secrets/index.md
@@ -1,6 +1,6 @@
---
-stage: Configure
-group: Configure
+stage: Verify
+group: Pipeline Authoring
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: concepts, howto
---
diff --git a/doc/install/installation.md b/doc/install/installation.md
index f405bc40f43..898ff59585c 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -1041,9 +1041,8 @@ To use GitLab with HTTPS:
1. Update `ssl_certificate` and `ssl_certificate_key`.
1. Review the configuration file and consider applying other security and performance enhancing features.
-Using a self-signed certificate is discouraged but if you must use it, follow the normal directions. Then:
-
-1. Generate a self-signed SSL certificate:
+Using a self-signed certificate is discouraged. If you must use one,
+follow the normal directions and generate a self-signed SSL certificate:
```shell
mkdir -p /etc/nginx/ssl/
@@ -1052,7 +1051,12 @@ Using a self-signed certificate is discouraged but if you must use it, follow th
sudo chmod o-r gitlab.key
```
-1. In the `config.yml` of GitLab Shell set `self_signed_cert` to `true`.
+WARNING:
+The `self_signed_cert` variable is
+[deprecated and redundant](https://gitlab.com/gitlab-org/gitlab-shell/-/issues/120).
+It is set to `false` by default, but still accepts self-signed certificates. Setting
+this value to `true` allows any certificate to be accepted, and can make
+machine-in-the-middle attacks possible.
### Enable Reply by email
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 36c49e39151..5efb31868e6 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -73,8 +73,7 @@ The following table lists project permissions available for each role:
| [CI/CD](../ci/index.md):<br>View a job with [debug logging](../ci/variables/index.md#debug-logging) | | | ✓ | ✓ | ✓ |
| [CI/CD](../ci/index.md):<br>Manage CI/CD variables | | | | ✓ | ✓ |
| [CI/CD](../ci/index.md):<br>Manage job triggers | | | | ✓ | ✓ |
-| [CI/CD](../ci/index.md):<br>Manage group runners | | | | | ✓ |
-| [CI/CD](../ci/index.md):<br>Manage project runners | | | | ✓ | ✓ |
+| [CI/CD](../ci/index.md):<br>Manage runners | | | | ✓ | ✓ |
| [CI/CD](../ci/index.md):<br>Run Web IDE's Interactive Web Terminals **(ULTIMATE ONLY)** | | | | ✓ | ✓ |
| [CI/CD](../ci/index.md):<br>Use [environment terminals](../ci/environments/index.md#web-terminals-deprecated) | | | | ✓ | ✓ |
| [CI/CD](../ci/index.md):<br>Delete pipelines | | | | | ✓ |
@@ -293,56 +292,57 @@ The following table lists group permissions available for each role:
<!-- Keep this table sorted: first, by minimum role, then alphabetically. -->
-| Action | Guest | Reporter | Developer | Maintainer | Owner |
-|--------------------------------------------------------|-------|----------|-----------|------------|-------|
-| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
-| Edit SAML SSO Billing **(PREMIUM SAAS)** | ✓ | ✓ | ✓ | ✓ | ✓ (4) |
-| Pull a container image using the dependency proxy | ✓ | ✓ | ✓ | ✓ | ✓ |
-| View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
-| View group epic **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
-| View group wiki pages **(PREMIUM)** | ✓ (6) | ✓ | ✓ | ✓ | ✓ |
-| View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
-| View Insights charts **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
-| View Issue analytics **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
-| View Value Stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
-| Create/edit group epic **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
-| Create/edit/delete epic boards **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
-| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
-| Pull [packages](packages/index.md) | | ✓ | ✓ | ✓ | ✓ |
-| View a container registry | | ✓ | ✓ | ✓ | ✓ |
-| View Group DevOps Adoption **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
-| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |
-| View Productivity analytics **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
-| Create and edit group wiki pages **(PREMIUM)** | | | ✓ | ✓ | ✓ |
-| Create project in group | | | ✓ (3)(5) | ✓ (3) | ✓ (3) |
-| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
-| Create/edit/delete iterations | | | ✓ | ✓ | ✓ |
-| Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ |
-| Enable/disable a dependency proxy | | | ✓ | ✓ | ✓ |
-| Purge the dependency proxy for a group | | | | | ✓ |
-| Publish [packages](packages/index.md) | | | ✓ | ✓ | ✓ |
-| Use security dashboard **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
-| View group Audit Events | | | ✓ (7) | ✓ (7) | ✓ |
-| Create subgroup | | | | ✓ (1) | ✓ |
-| Delete group wiki pages **(PREMIUM)** | | | ✓ | ✓ | ✓ |
-| Edit epic comments (posted by any user) **(ULTIMATE)** | | | | ✓ (2) | ✓ (2) |
-| List group deploy tokens | | | | ✓ | ✓ |
-| Manage [group push rules](group/index.md#group-push-rules) **(PREMIUM)** | | | | ✓ | ✓ |
-| View/manage group-level Kubernetes cluster | | | | ✓ | ✓ |
-| Administer project compliance frameworks | | | | | ✓ |
-| Create/Delete group deploy tokens | | | | | ✓ |
-| Change group visibility level | | | | | ✓ |
-| Delete group | | | | | ✓ |
-| Delete group epic **(PREMIUM)** | | | | | ✓ |
-| Disable notification emails | | | | | ✓ |
-| Edit group settings | | | | | ✓ |
-| Filter members by 2FA status | | | | | ✓ |
-| Manage group level CI/CD variables | | | | | ✓ |
-| Manage group members | | | | | ✓ |
-| Share (invite) groups with groups | | | | | ✓ |
-| View 2FA status of members | | | | | ✓ |
-| View Billing **(FREE SAAS)** | | | | | ✓ (4) |
-| View Usage Quotas **(FREE SAAS)** | | | | | ✓ (4) |
+| Action | Guest | Reporter | Developer | Maintainer | Owner |
+|--------------------------------------------------------------------------|-------|----------|-----------|------------|-------|
+| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Edit SAML SSO Billing **(PREMIUM SAAS)** | ✓ | ✓ | ✓ | ✓ | ✓ (4) |
+| Pull a container image using the dependency proxy | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View group epic **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View group wiki pages **(PREMIUM)** | ✓ (6) | ✓ | ✓ | ✓ | ✓ |
+| View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View Insights charts **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View Issue analytics **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
+| View Value Stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Create/edit group epic **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
+| Create/edit/delete epic boards **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
+| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
+| Pull [packages](packages/index.md) | | ✓ | ✓ | ✓ | ✓ |
+| View a container registry | | ✓ | ✓ | ✓ | ✓ |
+| View Group DevOps Adoption **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
+| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |
+| View Productivity analytics **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
+| Create and edit group wiki pages **(PREMIUM)** | | | ✓ | ✓ | ✓ |
+| Create project in group | | | ✓ (3)(5) | ✓ (3) | ✓ (3) |
+| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
+| Create/edit/delete iterations | | | ✓ | ✓ | ✓ |
+| Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ |
+| Enable/disable a dependency proxy | | | ✓ | ✓ | ✓ |
+| Purge the dependency proxy for a group | | | | | ✓ |
+| Publish [packages](packages/index.md) | | | ✓ | ✓ | ✓ |
+| Use security dashboard **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
+| View group Audit Events | | | ✓ (7) | ✓ (7) | ✓ |
+| Create subgroup | | | | ✓ (1) | ✓ |
+| Delete group wiki pages **(PREMIUM)** | | | ✓ | ✓ | ✓ |
+| Edit epic comments (posted by any user) **(ULTIMATE)** | | | | ✓ (2) | ✓ (2) |
+| List group deploy tokens | | | | ✓ | ✓ |
+| Manage [group push rules](group/index.md#group-push-rules) **(PREMIUM)** | | | | ✓ | ✓ |
+| View/manage group-level Kubernetes cluster | | | | ✓ | ✓ |
+| Administer project compliance frameworks | | | | | ✓ |
+| Create/Delete group deploy tokens | | | | | ✓ |
+| Change group visibility level | | | | | ✓ |
+| Delete group | | | | | ✓ |
+| Delete group epic **(PREMIUM)** | | | | | ✓ |
+| Disable notification emails | | | | | ✓ |
+| Edit group settings | | | | | ✓ |
+| Filter members by 2FA status | | | | | ✓ |
+| Manage group level CI/CD variables | | | | | ✓ |
+| Manage group members | | | | | ✓ |
+| Share (invite) groups with groups | | | | | ✓ |
+| View 2FA status of members | | | | | ✓ |
+| View Billing **(FREE SAAS)** | | | | | ✓ (4) |
+| View Usage Quotas **(FREE SAAS)** | | | | | ✓ (4) |
+| Manage runners | | | | | ✓ |
1. Groups can be set to [allow either Owners or Owners and
Maintainers to create subgroups](group/subgroups/index.md#creating-a-subgroup)
diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md
index 4068d8e056c..eb18834cc6b 100644
--- a/doc/user/project/code_owners.md
+++ b/doc/user/project/code_owners.md
@@ -281,7 +281,7 @@ README.md @docs
### Approvals shown as optional
-A Code Owner approval rule is optional if these conditions are not met:
+A Code Owner approval rule is optional if any of these conditions are true:
- The user or group are not a member of the project or parent group.
- [Code Owner approval on a protected branch](protected_branches.md#require-code-owner-approval-on-a-protected-branch) has not been set up.
diff --git a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
index 2e3db2a5c6e..1dcbd86f1cd 100644
--- a/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
+++ b/lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb
@@ -87,6 +87,8 @@ module Gitlab
all_tables = context[:modified_tables_by_db].values.map(&:to_a).flatten
schemas = ::Gitlab::Database::GitlabSchema.table_schemas(all_tables)
+ schemas += ApplicationRecord.gitlab_transactions_stack
+
if schemas.many?
message = "Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
"a transaction modifying the '#{all_tables.to_a.join(", ")}' tables." \
diff --git a/lib/gitlab/import_export/project/object_builder.rb b/lib/gitlab/import_export/project/object_builder.rb
index f7598ba1337..1baad4d3aaa 100644
--- a/lib/gitlab/import_export/project/object_builder.rb
+++ b/lib/gitlab/import_export/project/object_builder.rb
@@ -14,8 +14,10 @@ module Gitlab
# It also adds some logic around Group Labels/Milestones for edge cases.
class ObjectBuilder < Base::ObjectBuilder
def self.build(*args)
- ::Project.transaction do
- super
+ ::Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/350091') do
+ ::Project.transaction do
+ super
+ end
end
end
diff --git a/spec/features/boards/board_filters_spec.rb b/spec/features/boards/board_filters_spec.rb
index 49375e4b37b..783b701991b 100644
--- a/spec/features/boards/board_filters_spec.rb
+++ b/spec/features/boards/board_filters_spec.rb
@@ -7,8 +7,8 @@ RSpec.describe 'Issue board filters', :js do
let_it_be(:user) { create(:user) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:project_label) { create(:label, project: project, title: 'Label') }
- let_it_be(:milestone_1) { create(:milestone, project: project) }
- let_it_be(:milestone_2) { create(:milestone, project: project) }
+ let_it_be(:milestone_1) { create(:milestone, project: project, due_date: 3.days.from_now ) }
+ let_it_be(:milestone_2) { create(:milestone, project: project, due_date: Date.tomorrow ) }
let_it_be(:release) { create(:release, tag: 'v1.0', project: project, milestones: [milestone_1]) }
let_it_be(:release_2) { create(:release, tag: 'v2.0', project: project, milestones: [milestone_2]) }
let_it_be(:issue_1) { create(:issue, project: project, milestone: milestone_1, author: user) }
@@ -134,8 +134,11 @@ RSpec.describe 'Issue board filters', :js do
expect(filter_dropdown).to have_content('Any')
expect(filter_dropdown).to have_content('Started')
expect(filter_dropdown).to have_content('Upcoming')
- expect(filter_dropdown).to have_content(milestone_1.title)
- expect(filter_dropdown).to have_content(milestone_2.title)
+
+ dropdown_nodes = page.find_all('.gl-filtered-search-suggestion-list > .gl-filtered-search-suggestion')
+
+ expect(dropdown_nodes[4]).to have_content(milestone_2.title)
+ expect(dropdown_nodes.last).to have_content(milestone_1.title)
click_on milestone_1.title
filter_submit.click
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index b0fb94d2b29..ee2f6541b03 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -2,22 +2,14 @@ import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea
import { mount } from '@vue/test-utils';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
+import { mockField } from '../mock_data';
describe('DynamicField', () => {
let wrapper;
- const defaultProps = {
- help: 'The URL of the project',
- name: 'project_url',
- placeholder: 'https://jira.example.com',
- title: 'Project URL',
- type: 'text',
- value: '1',
- };
-
const createComponent = (props, isInheriting = false) => {
wrapper = mount(DynamicField, {
- propsData: { ...defaultProps, ...props },
+ propsData: { ...mockField, ...props },
computed: {
isInheriting: () => isInheriting,
},
@@ -61,7 +53,7 @@ describe('DynamicField', () => {
});
it(`renders GlFormCheckbox with correct text content when checkboxLabel is ${checkboxLabel}`, () => {
- expect(findGlFormCheckbox().text()).toContain(checkboxLabel ?? defaultProps.title);
+ expect(findGlFormCheckbox().text()).toContain(checkboxLabel ?? mockField.title);
});
it('does not render other types of input', () => {
@@ -160,7 +152,7 @@ describe('DynamicField', () => {
type: 'text',
id: 'service_project_url',
name: 'service[project_url]',
- placeholder: defaultProps.placeholder,
+ placeholder: mockField.placeholder,
required: 'required',
});
expect(findGlFormInput().attributes('readonly')).toBe(readonly);
@@ -179,7 +171,7 @@ describe('DynamicField', () => {
it('renders description with help text', () => {
createComponent();
- expect(findGlFormGroup().find('small').text()).toBe(defaultProps.help);
+ expect(findGlFormGroup().find('small').text()).toBe(mockField.help);
});
describe('when type is checkbox', () => {
@@ -189,7 +181,7 @@ describe('DynamicField', () => {
});
expect(findGlFormGroup().find('small').exists()).toBe(false);
- expect(findGlFormCheckbox().text()).toContain(defaultProps.help);
+ expect(findGlFormCheckbox().text()).toContain(mockField.help);
});
});
@@ -221,40 +213,36 @@ describe('DynamicField', () => {
it('renders label with title', () => {
createComponent();
- expect(findGlFormGroup().find('label').text()).toBe(defaultProps.title);
+ expect(findGlFormGroup().find('label').text()).toBe(mockField.title);
});
});
- describe('validations', () => {
- describe('password field', () => {
- beforeEach(() => {
+ describe('password field validations', () => {
+ describe('without value', () => {
+ it('requires validation', () => {
createComponent({
type: 'password',
required: true,
value: null,
+ isValidated: true,
});
- wrapper.vm.validated = true;
- });
-
- describe('without value', () => {
- it('requires validation', () => {
- expect(wrapper.vm.valid).toBe(false);
- expect(findGlFormGroup().classes('is-invalid')).toBe(true);
- expect(findGlFormInput().classes('is-invalid')).toBe(true);
- });
+ expect(findGlFormGroup().classes('is-invalid')).toBe(true);
+ expect(findGlFormInput().classes('is-invalid')).toBe(true);
});
+ });
- describe('with value', () => {
- beforeEach(() => {
- wrapper.setProps({ value: 'true' });
+ describe('with value', () => {
+ it('does not require validation', () => {
+ createComponent({
+ type: 'password',
+ required: true,
+ value: 'test value',
+ isValidated: true,
});
- it('does not require validation', () => {
- expect(wrapper.vm.valid).toBe(true);
- expect(findGlFormGroup().classes('is-valid')).toBe(true);
- expect(findGlFormInput().classes('is-valid')).toBe(true);
- });
+ expect(findGlFormGroup().classes('is-valid')).toBe(true);
+ expect(findGlFormInput().classes('is-valid')).toBe(true);
});
});
});
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 8cf8a403e5d..2f123508c06 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -17,16 +17,13 @@ import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import {
integrationLevels,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
- VALIDATE_INTEGRATION_FORM_EVENT,
I18N_DEFAULT_ERROR_MESSAGE,
} from '~/integrations/constants';
import { createStore } from '~/integrations/edit/store';
-import eventHub from '~/integrations/edit/event_hub';
import httpStatus from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import { mockIntegrationProps } from '../mock_data';
+import { mockIntegrationProps, mockField } from '../mock_data';
-jest.mock('~/integrations/edit/event_hub');
jest.mock('@sentry/browser');
jest.mock('~/lib/utils/url_utility');
@@ -97,6 +94,7 @@ describe('IntegrationForm', () => {
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
const findFormElement = () => (vueIntegrationFormFeatureFlag ? findGlForm().element : mockForm);
+ const findDynamicField = () => wrapper.findComponent(DynamicField);
const mockFormFunctions = ({ checkValidityReturn }) => {
jest.spyOn(findFormElement(), 'checkValidity').mockReturnValue(checkValidityReturn);
@@ -490,6 +488,7 @@ describe('IntegrationForm', () => {
showActive: true,
canTest: true,
initialActivated: true,
+ fields: [mockField],
},
mountFn: mountExtended,
});
@@ -510,27 +509,28 @@ describe('IntegrationForm', () => {
expect(findTestButton().props('disabled')).toBe(false);
});
- it('emits `VALIDATE_INTEGRATION_FORM_EVENT`', () => {
- expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
+ it('sets `isValidated` props on form fields', () => {
+ expect(findDynamicField().props('isValidated')).toBe(true);
});
});
});
describe('when `test` button is clicked', () => {
describe('when form is invalid', () => {
- it('emits `VALIDATE_INTEGRATION_FORM_EVENT` event to the event hub', () => {
+ it('sets `isValidated` props on form fields', async () => {
createComponent({
customStateProps: {
showActive: true,
canTest: true,
+ fields: [mockField],
},
mountFn: mountExtended,
});
mockFormFunctions({ checkValidityReturn: false });
- findTestButton().vm.$emit('click', new Event('click'));
+ await findTestButton().vm.$emit('click', new Event('click'));
- expect(eventHub.$emit).toHaveBeenCalledWith(VALIDATE_INTEGRATION_FORM_EVENT);
+ expect(findDynamicField().props('isValidated')).toBe(true);
});
});
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index b5a8eed3598..18afbf25bf0 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -1,9 +1,7 @@
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { VALIDATE_INTEGRATION_FORM_EVENT } from '~/integrations/constants';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
-import eventHub from '~/integrations/edit/event_hub';
import { createStore } from '~/integrations/edit/store';
describe('JiraIssuesFields', () => {
@@ -222,7 +220,7 @@ describe('JiraIssuesFields', () => {
});
describe('Project key input field', () => {
- beforeEach(() => {
+ it('sets Project Key `state` attribute to `true` by default', () => {
createComponent({
props: {
initialProjectKey: '',
@@ -230,29 +228,32 @@ describe('JiraIssuesFields', () => {
},
mountFn: shallowMountExtended,
});
- });
- it('sets Project Key `state` attribute to `true` by default', () => {
assertProjectKeyState('true');
});
- describe('when event hub recieves `VALIDATE_INTEGRATION_FORM_EVENT` event', () => {
+ describe('when `isValidated` prop is true', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ initialProjectKey: '',
+ initialEnableJiraIssues: true,
+ isValidated: true,
+ },
+ mountFn: shallowMountExtended,
+ });
+ });
+
describe('with no project key', () => {
it('sets Project Key `state` attribute to `undefined`', async () => {
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
- await wrapper.vm.$nextTick();
-
assertProjectKeyState(undefined);
});
});
describe('when project key is set', () => {
it('sets Project Key `state` attribute to `true`', async () => {
- eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT);
-
// set the project key
await findProjectKey().vm.$emit('input', 'AB');
- await wrapper.vm.$nextTick();
assertProjectKeyState('true');
});
diff --git a/spec/frontend/integrations/edit/mock_data.js b/spec/frontend/integrations/edit/mock_data.js
index 3c45ed0fb1b..39e5f8521e8 100644
--- a/spec/frontend/integrations/edit/mock_data.js
+++ b/spec/frontend/integrations/edit/mock_data.js
@@ -20,3 +20,12 @@ export const mockJiraIssueTypes = [
{ id: '2', name: 'bug', description: 'bug' },
{ id: '3', name: 'epic', description: 'epic' },
];
+
+export const mockField = {
+ help: 'The URL of the project',
+ name: 'project_url',
+ placeholder: 'https://jira.example.com',
+ title: 'Project URL',
+ type: 'text',
+ value: '1',
+};
diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
index 6caf36e1461..263698e94e1 100644
--- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
@@ -1,13 +1,16 @@
import { GlModal } from '@gitlab/ui';
import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ActionsCell from '~/jobs/components/table/cells/actions_cell.vue';
import JobPlayMutation from '~/jobs/components/table/graphql/mutations/job_play.mutation.graphql';
import JobRetryMutation from '~/jobs/components/table/graphql/mutations/job_retry.mutation.graphql';
import JobUnscheduleMutation from '~/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql';
+import JobCancelMutation from '~/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql';
import {
playableJob,
retryableJob,
+ cancelableJob,
scheduledJob,
cannotRetryJob,
cannotPlayJob,
@@ -20,6 +23,7 @@ describe('Job actions cell', () => {
const findRetryButton = () => wrapper.findByTestId('retry');
const findPlayButton = () => wrapper.findByTestId('play');
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
const findDownloadArtifactsButton = () => wrapper.findByTestId('download-artifacts');
const findCountdownButton = () => wrapper.findByTestId('countdown');
const findPlayScheduledJobButton = () => wrapper.findByTestId('play-scheduled');
@@ -32,6 +36,7 @@ describe('Job actions cell', () => {
data: { JobUnscheduleMutation: { jobId: scheduledJob.id } },
};
const MUTATION_SUCCESS_PLAY = { data: { JobPlayMutation: { jobId: playableJob.id } } };
+ const MUTATION_SUCCESS_CANCEL = { data: { JobCancelMutation: { jobId: cancelableJob.id } } };
const $toast = {
show: jest.fn(),
@@ -88,6 +93,7 @@ describe('Job actions cell', () => {
${findPlayButton} | ${'play'} | ${playableJob}
${findRetryButton} | ${'retry'} | ${retryableJob}
${findDownloadArtifactsButton} | ${'download artifacts'} | ${playableJob}
+ ${findCancelButton} | ${'cancel'} | ${cancelableJob}
`('displays the $action button', ({ button, jobType }) => {
createComponent(jobType);
@@ -95,9 +101,10 @@ describe('Job actions cell', () => {
});
it.each`
- button | mutationResult | action | jobType | mutationFile
- ${findPlayButton} | ${MUTATION_SUCCESS_PLAY} | ${'play'} | ${playableJob} | ${JobPlayMutation}
- ${findRetryButton} | ${MUTATION_SUCCESS} | ${'retry'} | ${retryableJob} | ${JobRetryMutation}
+ button | mutationResult | action | jobType | mutationFile
+ ${findPlayButton} | ${MUTATION_SUCCESS_PLAY} | ${'play'} | ${playableJob} | ${JobPlayMutation}
+ ${findRetryButton} | ${MUTATION_SUCCESS} | ${'retry'} | ${retryableJob} | ${JobRetryMutation}
+ ${findCancelButton} | ${MUTATION_SUCCESS_CANCEL} | ${'cancel'} | ${cancelableJob} | ${JobCancelMutation}
`('performs the $action mutation', ({ button, mutationResult, jobType, mutationFile }) => {
createComponent(jobType, mutationResult);
@@ -111,6 +118,24 @@ describe('Job actions cell', () => {
});
});
+ it.each`
+ button | action | jobType
+ ${findPlayButton} | ${'play'} | ${playableJob}
+ ${findRetryButton} | ${'retry'} | ${retryableJob}
+ ${findCancelButton} | ${'cancel'} | ${cancelableJob}
+ ${findUnscheduleButton} | ${'unschedule'} | ${scheduledJob}
+ `('disables the $action button after first request', async ({ button, jobType }) => {
+ createComponent(jobType);
+
+ expect(button().props('disabled')).toBe(false);
+
+ button().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(button().props('disabled')).toBe(true);
+ });
+
describe('Scheduled Jobs', () => {
const today = () => new Date('2021-08-31');
diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js
index 45d297ba364..4aabefc1f66 100644
--- a/spec/frontend/jobs/mock_data.js
+++ b/spec/frontend/jobs/mock_data.js
@@ -1653,6 +1653,65 @@ export const retryableJob = {
__typename: 'CiJob',
};
+export const cancelableJob = {
+ artifacts: {
+ nodes: [],
+ __typename: 'CiJobArtifactConnection',
+ },
+ allowFailure: false,
+ status: 'PENDING',
+ scheduledAt: null,
+ manualJob: false,
+ triggered: null,
+ createdByTag: false,
+ detailedStatus: {
+ id: 'pending-1305-1305',
+ detailsPath: '/root/lots-of-jobs-project/-/jobs/1305',
+ group: 'pending',
+ icon: 'status_pending',
+ label: 'pending',
+ text: 'pending',
+ tooltip: 'pending',
+ action: {
+ id: 'Ci::Build-pending-1305',
+ buttonTitle: 'Cancel this job',
+ icon: 'cancel',
+ method: 'post',
+ path: '/root/lots-of-jobs-project/-/jobs/1305/cancel',
+ title: 'Cancel',
+ __typename: 'StatusAction',
+ },
+ __typename: 'DetailedStatus',
+ },
+ id: 'gid://gitlab/Ci::Build/1305',
+ refName: 'main',
+ refPath: '/root/lots-of-jobs-project/-/commits/main',
+ tags: [],
+ shortSha: '750605f2',
+ commitPath: '/root/lots-of-jobs-project/-/commit/750605f29530778cf0912779eba6d073128962a5',
+ stage: {
+ id: 'gid://gitlab/Ci::Stage/181',
+ name: 'deploy',
+ __typename: 'CiStage',
+ },
+ name: 'job_212',
+ duration: null,
+ finishedAt: null,
+ coverage: null,
+ retryable: false,
+ playable: false,
+ cancelable: true,
+ active: true,
+ stuck: false,
+ userPermissions: {
+ readBuild: true,
+ readJobArtifacts: true,
+ updateBuild: true,
+ __typename: 'JobPermissions',
+ },
+ __typename: 'CiJob',
+};
+
export const cannotRetryJob = {
...retryableJob,
userPermissions: { readBuild: true, updateBuild: false, __typename: 'JobPermissions' },
diff --git a/spec/graphql/resolvers/package_pipelines_resolver_spec.rb b/spec/graphql/resolvers/package_pipelines_resolver_spec.rb
index 024f96e7158..892dc641201 100644
--- a/spec/graphql/resolvers/package_pipelines_resolver_spec.rb
+++ b/spec/graphql/resolvers/package_pipelines_resolver_spec.rb
@@ -15,8 +15,9 @@ RSpec.describe Resolvers::PackagePipelinesResolver do
subject { resolve(described_class, obj: package, args: args, ctx: { current_user: user }) }
before do
- package.pipelines = pipelines
- package.save!
+ pipelines.each do |pipeline|
+ create(:package_build_info, package: package, pipeline: pipeline)
+ end
end
it { is_expected.to contain_exactly(*pipelines) }
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
index c41b4eeea10..ebd3c5f6235 100644
--- a/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_cross_database_modification_spec.rb
@@ -14,23 +14,25 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
Gitlab::Database::QueryAnalyzer.instance.within { example.run }
end
- shared_examples 'successful examples' do
+ shared_examples 'successful examples' do |model:|
+ let(:model) { model }
+
context 'outside transaction' do
it { expect { run_queries }.not_to raise_error }
end
- context 'within transaction' do
+ context "within #{model} transaction" do
it do
- Project.transaction do
+ model.transaction do
expect { run_queries }.not_to raise_error
end
end
end
- context 'within nested transaction' do
+ context "within nested #{model} transaction" do
it do
- Project.transaction(requires_new: true) do
- Project.transaction(requires_new: true) do
+ model.transaction(requires_new: true) do
+ model.transaction(requires_new: true) do
expect { run_queries }.not_to raise_error
end
end
@@ -38,13 +40,26 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
end
end
+ shared_examples 'cross-database modification errors' do |model:|
+ let(:model) { model }
+
+ context "within #{model} transaction" do
+ it 'raises error' do
+ model.transaction do
+ expect { run_queries }.to raise_error /Cross-database data modification/
+ end
+ end
+ end
+ end
+
context 'when CI and other tables are read in a transaction' do
def run_queries
pipeline.reload
project.reload
end
- include_examples 'successful examples'
+ include_examples 'successful examples', model: Project
+ include_examples 'successful examples', model: Ci::Pipeline
end
context 'when only CI data is modified' do
@@ -53,7 +68,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
project.reload
end
- include_examples 'successful examples'
+ include_examples 'successful examples', model: Ci::Pipeline
+
+ include_examples 'cross-database modification errors', model: Project
end
context 'when other data is modified' do
@@ -62,7 +79,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
project.touch
end
- include_examples 'successful examples'
+ include_examples 'successful examples', model: Project
+
+ include_examples 'cross-database modification errors', model: Ci::Pipeline
end
context 'when both CI and other data is modified' do
@@ -144,7 +163,9 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModificatio
project.save!
end
- include_examples 'successful examples'
+ include_examples 'successful examples', model: Ci::Pipeline
+
+ include_examples 'cross-database modification errors', model: Project
end
describe '.allow_cross_database_modification_within_transaction' do
diff --git a/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb b/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb
new file mode 100644
index 00000000000..b3fe1776183
--- /dev/null
+++ b/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe PopulateAuditEventStreamingVerificationToken do
+ let(:groups) { table(:namespaces) }
+ let(:destinations) { table(:audit_events_external_audit_event_destinations) }
+ let(:migration) { described_class.new }
+
+ let!(:group) { groups.create!(name: 'test-group', path: 'test-group') }
+ let!(:destination) { destinations.create!(namespace_id: group.id, destination_url: 'https://example.com/destination', verification_token: nil) }
+
+ describe '#up' do
+ it 'adds verification tokens to records created before the migration' do
+ expect do
+ migrate!
+ destination.reload
+ end.to change { destination.verification_token }.from(nil).to(a_string_matching(/\w{24}/))
+ end
+ end
+end
diff --git a/spec/models/concerns/cross_database_modification_spec.rb b/spec/models/concerns/cross_database_modification_spec.rb
new file mode 100644
index 00000000000..72544536953
--- /dev/null
+++ b/spec/models/concerns/cross_database_modification_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe CrossDatabaseModification do
+ describe '.transaction' do
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(track_gitlab_schema_in_current_transaction: false)
+ end
+
+ it 'does not add to gitlab_transactions_stack' do
+ ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+ end
+ end
+
+ context 'feature flag is not yet setup' do
+ before do
+ allow(Feature::FlipperFeature).to receive(:table_exists?).and_raise(ActiveRecord::NoDatabaseError)
+ end
+
+ it 'does not add to gitlab_transactions_stack' do
+ ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+ end
+ end
+
+ it 'adds the current gitlab schema to gitlab_transactions_stack', :aggregate_failures do
+ ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main)
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ Ci::ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_ci)
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ Project.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main)
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ Ci::Pipeline.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_ci)
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
+ ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main)
+
+ Ci::Pipeline.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main, :gitlab_ci)
+
+ Project.first
+ end
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+ end
+
+ it 'yields' do
+ expect { |block| ApplicationRecord.transaction(&block) }.to yield_control
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/packages/package_spec.rb b/spec/requests/api/graphql/packages/package_spec.rb
index 2ff3bc7cc47..a9b7149d7c4 100644
--- a/spec/requests/api/graphql/packages/package_spec.rb
+++ b/spec/requests/api/graphql/packages/package_spec.rb
@@ -145,8 +145,9 @@ RSpec.describe 'package details' do
let(:pipeline_gids) { pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse }
before do
- composer_package.pipelines = pipelines
- composer_package.save!
+ pipelines.each do |pipeline|
+ create(:package_build_info, package: composer_package, pipeline: pipeline)
+ end
end
def run_query(args)