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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-01-27 18:14:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-01-27 18:14:51 +0300
commit48f93eadd0c117a41b4e29e3d335f451a5e6e52f (patch)
treeb7ca830e2b28309a5a1fd61a6b6c646485d710f2
parent115190b6cd73b4ba1948cf26ea883738934c614b (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/database/multiple_databases.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/environments/components/deployment.vue120
-rw-r--r--app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue3
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue60
-rw-r--r--app/assets/javascripts/runner/components/runner_header.vue2
-rw-r--r--app/assets/javascripts/runner/components/runner_pause_button.vue122
-rw-r--r--app/assets/javascripts/runner/constants.js6
-rw-r--r--app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql14
-rw-r--r--app/assets/javascripts/runner/graphql/runner_toggle_active.mutation.graphql12
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue6
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue12
-rw-r--r--app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql15
-rw-r--r--app/assets/javascripts/security_configuration/index.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/issues.js4
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue4
-rw-r--r--app/finders/group_descendants_finder.rb9
-rw-r--r--app/views/projects/security/configuration/show.html.haml2
-rw-r--r--app/workers/auto_devops/disable_worker.rb8
-rw-r--r--app/workers/project_export_worker.rb15
-rw-r--r--config/feature_flags/development/ci_order_subsequent_jobs_by_stage.yml2
-rw-r--r--config/feature_flags/development/linear_group_descendants_finder_upto.yml (renamed from config/feature_flags/development/export_reduce_relation_batch_size.yml)10
-rw-r--r--db/migrate/20210316171009_create_packages_helm_file_metadata.rb8
-rw-r--r--db/migrate/20210504153354_create_clusters_integration_elasticstack.rb8
-rw-r--r--db/migrate/20210628154900_create_detached_partitions_table.rb8
-rw-r--r--db/migrate/20210720140841_create_postgres_async_indexes_table.rb8
-rw-r--r--db/migrate/20210729081351_create_topics.rb8
-rw-r--r--db/post_migrate/20220126201752_remove_projects_ci_job_token_project_scope_links_target_project_id_fk.rb19
-rw-r--r--db/post_migrate/20220126210021_remove_projects_ci_builds_project_id_fk.rb20
-rw-r--r--db/schema_migrations/202201262017521
-rw-r--r--db/schema_migrations/202201262100211
-rw-r--r--db/structure.sql6
-rw-r--r--doc/integration/img/enable_trello_powerup.pngbin17791 -> 0 bytes
-rw-r--r--doc/integration/trello_power_up.md49
-rw-r--r--lib/gitlab/database/gitlab_loose_foreign_keys.yml6
-rw-r--r--lib/gitlab/database/migration_helpers.rb1
-rw-r--r--lib/gitlab/database/migrations/lock_retry_mixin.rb5
-rw-r--r--lib/gitlab/database/schema_helpers.rb1
-rw-r--r--lib/gitlab/database/with_lock_retries.rb2
-rw-r--r--lib/gitlab/import_export/json/streaming_serializer.rb9
-rw-r--r--locale/gitlab.pot32
-rw-r--r--spec/finders/group_descendants_finder_spec.rb86
-rw-r--r--spec/frontend/environments/deployment_spec.js32
-rw-r--r--spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js62
-rw-r--r--spec/frontend/runner/components/cells/runner_actions_cell_spec.js136
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js4
-rw-r--r--spec/frontend/runner/components/runner_pause_button_spec.js239
-rw-r--r--spec/frontend/security_configuration/components/app_spec.js24
-rw-r--r--spec/frontend/security_configuration/components/training_provider_list_spec.js47
-rw-r--r--spec/frontend/security_configuration/mock_data.js16
-rw-r--r--spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js3
-rw-r--r--spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js6
-rw-r--r--spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb18
-rw-r--r--spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb2
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb29
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb34
-rw-r--r--spec/models/ci/job_token/project_scope_link_spec.rb7
-rw-r--r--spec/models/commit_status_spec.rb7
-rw-r--r--spec/support/shared_examples/workers/project_export_shared_examples.rb16
-rw-r--r--spec/workers/auto_devops/disable_worker_spec.rb2
63 files changed, 969 insertions, 473 deletions
diff --git a/.rubocop_todo/database/multiple_databases.yml b/.rubocop_todo/database/multiple_databases.yml
index 7329d178dc1..28dddef7c79 100644
--- a/.rubocop_todo/database/multiple_databases.yml
+++ b/.rubocop_todo/database/multiple_databases.yml
@@ -19,7 +19,6 @@ Database/MultipleDatabases:
- lib/gitlab/database/migrations/observers/query_log.rb
- lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
- lib/gitlab/database.rb
- - lib/gitlab/database/with_lock_retries.rb
- lib/gitlab/gitlab_import/importer.rb
- lib/gitlab/health_checks/db_check.rb
- lib/gitlab/import_export/base/relation_factory.rb
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 8b63c5b3f5a..580ba589977 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-4b380e760d37508a2dc3c8e6a8fe1cfaae846916
+b7f0c0462a8f689c8ee9e654f0875157b238158b
diff --git a/app/assets/javascripts/environments/components/deployment.vue b/app/assets/javascripts/environments/components/deployment.vue
index 60ea837736c..b36d0d5f2e0 100644
--- a/app/assets/javascripts/environments/components/deployment.vue
+++ b/app/assets/javascripts/environments/components/deployment.vue
@@ -1,5 +1,6 @@
<script>
-import { GlBadge, GlIcon, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { GlBadge, GlButton, GlCollapse, GlIcon, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { __, s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -10,6 +11,8 @@ export default {
ClipboardButton,
DeploymentStatusBadge,
GlBadge,
+ GlButton,
+ GlCollapse,
GlIcon,
TimeAgoTooltip,
},
@@ -27,6 +30,9 @@ export default {
required: false,
},
},
+ data() {
+ return { visible: false };
+ },
computed: {
status() {
return this.deployment?.status;
@@ -40,43 +46,103 @@ export default {
createdAt() {
return this.deployment?.createdAt;
},
+ isMobile() {
+ return !GlBreakpointInstance.isDesktop();
+ },
+ detailsButton() {
+ return this.visible
+ ? { text: this.$options.i18n.hideDetails, icon: 'expand-up' }
+ : { text: this.$options.i18n.showDetails, icon: 'expand-down' };
+ },
+ detailsButtonClasses() {
+ return this.isMobile ? 'gl-sr-only' : '';
+ },
+ },
+ methods: {
+ toggleCollapse() {
+ this.visible = !this.visible;
+ },
},
i18n: {
latestBadge: s__('Deployment|Latest Deployed'),
deploymentId: s__('Deployment|Deployment ID'),
copyButton: __('Copy commit SHA'),
commitSha: __('Commit SHA'),
+ showDetails: __('Show details'),
+ hideDetails: __('Hide details'),
},
+ headerClasses: [
+ 'gl-display-flex',
+ 'gl-align-items-flex-start',
+ 'gl-md-align-items-center',
+ 'gl-justify-content-space-between',
+ 'gl-pr-6',
+ ],
+ headerDetailsClasses: [
+ 'gl-display-flex',
+ 'gl-flex-direction-column',
+ 'gl-md-flex-direction-row',
+ 'gl-align-items-flex-start',
+ 'gl-md-align-items-center',
+ 'gl-font-sm',
+ 'gl-text-gray-700',
+ ],
+ deploymentStatusClasses: [
+ 'gl-display-flex',
+ 'gl-gap-x-3',
+ 'gl-mr-0',
+ 'gl-md-mr-5',
+ 'gl-mb-3',
+ 'gl-md-mb-0',
+ ],
};
</script>
<template>
- <div class="gl-display-flex gl-align-items-center gl-gap-x-3 gl-font-sm gl-text-gray-700">
- <deployment-status-badge v-if="status" :status="status" />
- <gl-badge v-if="latest" variant="info">{{ $options.i18n.latestBadge }}</gl-badge>
- <div
- v-if="iid"
- v-gl-tooltip
- :title="$options.i18n.deploymentId"
- :aria-label="$options.i18n.deploymentId"
- >
- <gl-icon ref="deployment-iid-icon" name="deployments" /> #{{ iid }}
- </div>
- <div
- v-if="shortSha"
- data-testid="deployment-commit-sha"
- class="gl-font-monospace gl-display-flex gl-align-items-center"
- >
- <gl-icon ref="deployment-commit-icon" name="commit" class="gl-mr-2" />
- <span v-gl-tooltip :title="$options.i18n.commitSha">{{ shortSha }}</span>
- <clipboard-button
- :text="shortSha"
+ <div>
+ <div :class="$options.headerClasses">
+ <div :class="$options.headerDetailsClasses">
+ <div :class="$options.deploymentStatusClasses">
+ <deployment-status-badge v-if="status" :status="status" />
+ <gl-badge v-if="latest" variant="info">{{ $options.i18n.latestBadge }}</gl-badge>
+ </div>
+ <div class="gl-display-flex gl-align-items-center gl-gap-x-5">
+ <div
+ v-if="iid"
+ v-gl-tooltip
+ :title="$options.i18n.deploymentId"
+ :aria-label="$options.i18n.deploymentId"
+ >
+ <gl-icon ref="deployment-iid-icon" name="deployments" /> #{{ iid }}
+ </div>
+ <div
+ v-if="shortSha"
+ data-testid="deployment-commit-sha"
+ class="gl-font-monospace gl-display-flex gl-align-items-center"
+ >
+ <gl-icon ref="deployment-commit-icon" name="commit" class="gl-mr-2" />
+ <span v-gl-tooltip :title="$options.i18n.commitSha">{{ shortSha }}</span>
+ <clipboard-button
+ :text="shortSha"
+ category="tertiary"
+ :title="$options.i18n.copyButton"
+ size="small"
+ />
+ </div>
+ <time-ago-tooltip v-if="createdAt" :time="createdAt">
+ <template #default="{ timeAgo }"> <gl-icon name="calendar" /> {{ timeAgo }} </template>
+ </time-ago-tooltip>
+ </div>
+ </div>
+ <gl-button
+ ref="details-toggle"
category="tertiary"
- :title="$options.i18n.copyButton"
- size="small"
- />
- <time-ago-tooltip v-if="createdAt" :time="createdAt" class="gl-ml-5!">
- <template #default="{ timeAgo }"> <gl-icon name="calendar" /> {{ timeAgo }} </template>
- </time-ago-tooltip>
+ :icon="detailsButton.icon"
+ :button-text-classes="detailsButtonClasses"
+ @click="toggleCollapse"
+ >
+ {{ detailsButton.text }}
+ </gl-button>
</div>
+ <gl-collapse :visible="visible" />
</div>
</template>
diff --git a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue
index a7ae0213661..2795ddbbbcb 100644
--- a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue
+++ b/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue
@@ -4,6 +4,7 @@ import { createAlert } from '~/flash';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import RunnerEditButton from '../components/runner_edit_button.vue';
+import RunnerPauseButton from '../components/runner_pause_button.vue';
import RunnerHeader from '../components/runner_header.vue';
import RunnerDetails from '../components/runner_details.vue';
import { I18N_FETCH_ERROR } from '../constants';
@@ -14,6 +15,7 @@ export default {
name: 'AdminRunnerShowApp',
components: {
RunnerEditButton,
+ RunnerPauseButton,
RunnerHeader,
RunnerDetails,
},
@@ -66,6 +68,7 @@ export default {
<runner-header v-if="runner" :runner="runner">
<template #actions>
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
+ <runner-pause-button v-if="canUpdate" :runner="runner" />
</template>
</runner-header>
diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
index 19f6598c62c..ae9c774f2a2 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
@@ -1,16 +1,14 @@
<script>
import { GlButton, GlButtonGroup, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/flash';
-import { __, s__, sprintf } from '~/locale';
+import { s__, sprintf } from '~/locale';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
-import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
import { captureException } from '~/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerEditButton from '../runner_edit_button.vue';
+import RunnerPauseButton from '../runner_pause_button.vue';
import RunnerDeleteModal from '../runner_delete_modal.vue';
-const I18N_PAUSE = __('Pause');
-const I18N_RESUME = __('Resume');
const I18N_DELETE = s__('Runners|Delete runner');
const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
@@ -20,6 +18,7 @@ export default {
GlButton,
GlButtonGroup,
RunnerEditButton,
+ RunnerPauseButton,
RunnerDeleteModal,
},
directives: {
@@ -39,20 +38,6 @@ export default {
};
},
computed: {
- isActive() {
- return this.runner.active;
- },
- toggleActiveIcon() {
- return this.isActive ? 'pause' : 'play';
- },
- toggleActiveTitle() {
- if (this.updating) {
- // Prevent a "sticky" tooltip: If this button is disabled,
- // mouseout listeners don't run leaving the tooltip stuck
- return '';
- }
- return this.isActive ? I18N_PAUSE : I18N_RESUME;
- },
deleteTitle() {
if (this.deleting) {
// Prevent a "sticky" tooltip: If this button is disabled,
@@ -78,35 +63,6 @@ export default {
},
},
methods: {
- async onToggleActive() {
- this.updating = true;
- try {
- const toggledActive = !this.runner.active;
-
- const {
- data: {
- runnerUpdate: { errors },
- },
- } = await this.$apollo.mutate({
- mutation: runnerActionsUpdateMutation,
- variables: {
- input: {
- id: this.runner.id,
- active: toggledActive,
- },
- },
- });
-
- if (errors && errors.length) {
- throw new Error(errors.join(' '));
- }
- } catch (e) {
- this.onError(e);
- } finally {
- this.updating = false;
- }
- },
-
async onDelete() {
// Deleting stays "true" until this row is removed,
// should only change back if the operation fails.
@@ -162,15 +118,7 @@ export default {
See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
-->
<runner-edit-button v-if="canUpdate && runner.editAdminUrl" :href="runner.editAdminUrl" />
- <gl-button
- v-if="canUpdate"
- v-gl-tooltip.hover.viewport="toggleActiveTitle"
- :aria-label="toggleActiveTitle"
- :icon="toggleActiveIcon"
- :loading="updating"
- data-testid="toggle-active-runner"
- @click="onToggleActive"
- />
+ <runner-pause-button v-if="canUpdate" :runner="runner" :compact="true" />
<gl-button
v-if="canDelete"
v-gl-tooltip.hover.viewport="deleteTitle"
diff --git a/app/assets/javascripts/runner/components/runner_header.vue b/app/assets/javascripts/runner/components/runner_header.vue
index 295162e954a..abc07cec1ad 100644
--- a/app/assets/javascripts/runner/components/runner_header.vue
+++ b/app/assets/javascripts/runner/components/runner_header.vue
@@ -63,6 +63,6 @@ export default {
<strong>{{ heading }}</strong>
</template>
</div>
- <div class="gl-ml-auto"><slot name="actions"></slot></div>
+ <div class="gl-ml-auto gl-flex-shrink-0"><slot name="actions"></slot></div>
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_pause_button.vue b/app/assets/javascripts/runner/components/runner_pause_button.vue
new file mode 100644
index 00000000000..a8b259f5b90
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_pause_button.vue
@@ -0,0 +1,122 @@
+<script>
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import runnerToggleActiveMutation from '~/runner/graphql/runner_toggle_active.mutation.graphql';
+import { createAlert } from '~/flash';
+import { captureException } from '~/runner/sentry_utils';
+import { I18N_PAUSE, I18N_RESUME } from '../constants';
+
+export default {
+ name: 'RunnerPauseButton',
+ components: {
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ compact: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ updating: false,
+ };
+ },
+ computed: {
+ isActive() {
+ return this.runner.active;
+ },
+ icon() {
+ return this.isActive ? 'pause' : 'play';
+ },
+ label() {
+ return this.isActive ? I18N_PAUSE : I18N_RESUME;
+ },
+ buttonContent() {
+ if (this.compact) {
+ return null;
+ }
+ return this.label;
+ },
+ ariaLabel() {
+ if (this.compact) {
+ return this.label;
+ }
+ return null;
+ },
+ tooltip() {
+ // Only show tooltip when compact.
+ // Also prevent a "sticky" tooltip: If this button is
+ // disabled, mouseout listeners don't run leaving the tooltip stuck
+ if (this.compact && !this.updating) {
+ return this.label;
+ }
+ return '';
+ },
+ },
+ methods: {
+ async onToggle() {
+ this.updating = true;
+ try {
+ const input = {
+ id: this.runner.id,
+ active: !this.isActive,
+ };
+
+ const {
+ data: {
+ runnerUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: runnerToggleActiveMutation,
+ variables: {
+ input,
+ },
+ });
+
+ if (errors && errors.length) {
+ throw new Error(errors.join(' '));
+ }
+ } catch (e) {
+ this.onError(e);
+ } finally {
+ this.updating = false;
+ }
+ },
+ onError(error) {
+ const { message } = error;
+ createAlert({ message });
+
+ this.reportToSentry(error);
+ },
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button
+ v-gl-tooltip.hover.viewport="tooltip"
+ v-bind="$attrs"
+ :aria-label="ariaLabel"
+ :icon="icon"
+ :loading="updating"
+ @click="onToggle"
+ v-on="$listeners"
+ >
+ <!--
+ Use <template v-if> to ensure a square button is shown when compact: true.
+ Sending empty content will still show a distorted/rectangular button.
+ -->
+ <template v-if="buttonContent">{{ buttonContent }}</template>
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index ce8019ffaa0..ad0437d84ef 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
export const RUNNER_JOB_COUNT_LIMIT = 1000;
@@ -28,6 +28,10 @@ export const I18N_STALE_RUNNER_DESCRIPTION = s__(
'Runners|No contact from this runner in over 3 months',
);
+// Active flag
+export const I18N_PAUSE = __('Pause');
+export const I18N_RESUME = __('Resume');
+
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__('Runners|You cannot assign to other projects');
export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run jobs');
diff --git a/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql
deleted file mode 100644
index 547cc43907c..00000000000
--- a/app/assets/javascripts/runner/graphql/runner_actions_update.mutation.graphql
+++ /dev/null
@@ -1,14 +0,0 @@
-#import "~/runner/graphql/runner_node.fragment.graphql"
-
-# Mutation for updates within the runners list via action
-# buttons (play, pause, ...), loads attributes shown in the
-# runner list.
-
-mutation runnerActionsUpdate($input: RunnerUpdateInput!) {
- runnerUpdate(input: $input) {
- runner {
- ...RunnerNode
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/runner/graphql/runner_toggle_active.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_toggle_active.mutation.graphql
new file mode 100644
index 00000000000..9b15570dbc0
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/runner_toggle_active.mutation.graphql
@@ -0,0 +1,12 @@
+# Mutation executed for the pause/resume button in the
+# runner list and details views.
+
+mutation runnerToggleActive($input: RunnerUpdateInput!) {
+ runnerUpdate(input: $input) {
+ runner {
+ id
+ active
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index d228f77f27d..c48c9067250 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -50,7 +50,7 @@ export default {
TrainingProviderList,
},
mixins: [glFeatureFlagsMixin()],
- inject: ['projectPath'],
+ inject: ['projectFullPath'],
props: {
augmentedSecurityFeatures: {
type: Array,
@@ -107,14 +107,14 @@ export default {
shouldShowAutoDevopsEnabledAlert() {
return (
this.autoDevopsEnabled &&
- !this.autoDevopsEnabledAlertDismissedProjects.includes(this.projectPath)
+ !this.autoDevopsEnabledAlertDismissedProjects.includes(this.projectFullPath)
);
},
},
methods: {
dismissAutoDevopsEnabledAlert() {
const dismissedProjects = new Set(this.autoDevopsEnabledAlertDismissedProjects);
- dismissedProjects.add(this.projectPath);
+ dismissedProjects.add(this.projectFullPath);
this.autoDevopsEnabledAlertDismissedProjects = Array.from(dismissedProjects);
},
onError(message) {
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index ca4596e16b3..c289df9f1f4 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -21,10 +21,18 @@ export default {
GlLink,
GlSkeletonLoader,
},
- inject: ['projectPath'],
+ inject: ['projectFullPath'],
apollo: {
securityTrainingProviders: {
query: securityTrainingProvidersQuery,
+ variables() {
+ return {
+ fullPath: this.projectFullPath,
+ };
+ },
+ update({ project }) {
+ return project?.securityTrainingProviders;
+ },
error() {
this.errorMessage = this.$options.i18n.providerQueryErrorMessage;
},
@@ -68,7 +76,7 @@ export default {
variables: {
input: {
enabledProviders: enabledProviderIds,
- fullPath: this.projectPath,
+ fullPath: this.projectFullPath,
},
},
});
diff --git a/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql b/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql
index e0c5715ba8e..a8326bb1968 100644
--- a/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql
+++ b/app/assets/javascripts/security_configuration/graphql/security_training_providers.query.graphql
@@ -1,9 +1,12 @@
-query Query {
- securityTrainingProviders @client {
- name
+query getSecurityTrainingProviders($fullPath: ID!) {
+ project(fullPath: $fullPath) {
id
- description
- isEnabled
- url
+ securityTrainingProviders {
+ name
+ id
+ description
+ isEnabled
+ url
+ }
}
}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index 24c0585e077..c8255a010b2 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -19,7 +19,7 @@ export const initSecurityConfiguration = (el) => {
});
const {
- projectPath,
+ projectFullPath,
upgradePath,
features,
latestPipelinePath,
@@ -38,7 +38,7 @@ export const initSecurityConfiguration = (el) => {
el,
apolloProvider,
provide: {
- projectPath,
+ projectFullPath,
upgradePath,
autoDevopsHelpPagePath,
autoDevopsPath,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 96ee46c3b27..a25b4ab54e5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -128,10 +128,12 @@ export default {
api.trackRedisHllUserEvent(this.$options.expandEvent);
}
}),
- toggleCollapsed() {
- this.isCollapsed = !this.isCollapsed;
+ toggleCollapsed(e) {
+ if (!e?.target?.closest('.btn:not(.btn-icon),a')) {
+ this.isCollapsed = !this.isCollapsed;
- this.triggerRedisTracking();
+ this.triggerRedisTracking();
+ }
},
initExtensionPolling() {
const poll = new Poll({
@@ -207,6 +209,19 @@ export default {
this.showFade = true;
}
},
+ onRowMouseDown() {
+ this.down = Number(new Date());
+ },
+ onRowMouseUp(e) {
+ const up = Number(new Date());
+
+ // To allow for text to be selected we check if the the user is clicking
+ // or selecting, if they are selecting the time difference should be
+ // more than 200ms
+ if (up - this.down < 200) {
+ this.toggleCollapsed(e);
+ }
+ },
generateText,
},
EXTENSION_ICON_CLASS,
@@ -215,7 +230,7 @@ export default {
<template>
<section class="media-section" data-testid="widget-extension">
- <div class="media gl-p-5">
+ <div class="media gl-p-5 gl-cursor-pointer" @mousedown="onRowMouseDown" @mouseup="onRowMouseUp">
<status-icon
:name="$options.label || $options.name"
:is-loading="isLoadingSummary"
@@ -253,7 +268,7 @@ export default {
category="tertiary"
data-testid="toggle-button"
size="small"
- @click="toggleCollapsed"
+ @click.self="toggleCollapsed"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
index ba3336df2eb..4aeebf095c4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js
@@ -25,9 +25,9 @@ export default {
n__(
'ciReport|Load performance test metrics detected %{strong_start}%{changesFound}%{strong_end} change',
'ciReport|Load performance test metrics detected %{strong_start}%{changesFound}%{strong_end} changes',
- changesFound,
+ count,
),
- { changesFound },
+ { changesFound: count },
);
},
// Status icon to be used next to the summary text
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
index d1630c9ac13..3afd1f9410b 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
@@ -14,7 +14,7 @@ export default {
components: {
GlButton,
},
- inject: ['projectPath'],
+ inject: ['projectFullPath'],
props: {
feature: {
type: Object,
@@ -47,7 +47,7 @@ export default {
try {
const { mutationSettings } = this;
const { data } = await this.$apollo.mutate(
- mutationSettings.getMutationPayload(this.projectPath),
+ mutationSettings.getMutationPayload(this.projectFullPath),
);
const { errors, successPath } = data[mutationSettings.mutationId];
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index 3d9b6e94cc6..ff948d686ee 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -110,8 +110,13 @@ class GroupDescendantsFinder
# rubocop: disable CodeReuse/ActiveRecord
def ancestors_of_groups(base_for_ancestors)
group_ids = base_for_ancestors.except(:select, :sort).select(:id)
- Gitlab::ObjectHierarchy.new(Group.where(id: group_ids))
- .base_and_ancestors(upto: parent_group.id)
+ groups = Group.where(id: group_ids)
+
+ if Feature.enabled?(:linear_group_descendants_finder_upto, current_user, default_enabled: :yaml)
+ groups.self_and_ancestors(upto: parent_group.id)
+ else
+ Gitlab::ObjectHierarchy.new(groups).base_and_ancestors(upto: parent_group.id)
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/views/projects/security/configuration/show.html.haml b/app/views/projects/security/configuration/show.html.haml
index e8ac572df1d..c96d737a8b5 100644
--- a/app/views/projects/security/configuration/show.html.haml
+++ b/app/views/projects/security/configuration/show.html.haml
@@ -2,4 +2,4 @@
- page_title _("Security Configuration")
- @content_class = "limit-container-width" unless fluid_layout
-#js-security-configuration-static{ data: { project_path: @project.full_path, upgrade_path: security_upgrade_path } }
+#js-security-configuration-static{ data: { project_full_path: @project.full_path, upgrade_path: security_upgrade_path } }
diff --git a/app/workers/auto_devops/disable_worker.rb b/app/workers/auto_devops/disable_worker.rb
index 9ec3e5490c2..03613db3f29 100644
--- a/app/workers/auto_devops/disable_worker.rb
+++ b/app/workers/auto_devops/disable_worker.rb
@@ -32,8 +32,12 @@ module AutoDevops
def email_receivers_for(pipeline, project)
recipients = [pipeline.user&.email]
- recipients << project.owner.email unless project.group
- recipients.uniq.compact
+
+ if project.personal?
+ recipients << project.owners.map(&:email)
+ end
+
+ recipients.flatten.uniq.compact
end
end
end
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 4dd9a9c6fcb..e3f8c4bcd9d 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -24,8 +24,15 @@ class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker
::Projects::ImportExport::ExportService.new(project, current_user, params).execute(after_export)
export_job&.finish
- rescue ActiveRecord::RecordNotFound, Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError => e
- logger.error("Failed to export project #{project_id}: #{e.message}")
+ rescue ActiveRecord::RecordNotFound => e
+ log_failure(project_id, e)
+ rescue Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError => e
+ log_failure(project_id, e)
+ export_job&.finish
+ rescue StandardError => e
+ log_failure(project_id, e)
+ export_job&.fail_op
+ raise
end
private
@@ -35,4 +42,8 @@ class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker
Gitlab::ImportExport::AfterExportStrategyBuilder.build!(strategy_klass, after_export_strategy)
end
+
+ def log_failure(project_id, ex)
+ logger.error("Failed to export project #{project_id}: #{ex.message}")
+ end
end
diff --git a/config/feature_flags/development/ci_order_subsequent_jobs_by_stage.yml b/config/feature_flags/development/ci_order_subsequent_jobs_by_stage.yml
index dfc4ab3bad3..babe10764e3 100644
--- a/config/feature_flags/development/ci_order_subsequent_jobs_by_stage.yml
+++ b/config/feature_flags/development/ci_order_subsequent_jobs_by_stage.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349977
milestone: '14.7'
type: development
group: group::pipeline authoring
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/export_reduce_relation_batch_size.yml b/config/feature_flags/development/linear_group_descendants_finder_upto.yml
index 63164b6e9fe..ef045ebfa7a 100644
--- a/config/feature_flags/development/export_reduce_relation_batch_size.yml
+++ b/config/feature_flags/development/linear_group_descendants_finder_upto.yml
@@ -1,8 +1,8 @@
---
-name: export_reduce_relation_batch_size
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34057
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282245
-milestone: '13.1'
+name: linear_group_descendants_finder_upto
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78991
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350972
+milestone: '14.8'
type: development
-group: group::import
+group: group::authentication and authorization
default_enabled: false
diff --git a/db/migrate/20210316171009_create_packages_helm_file_metadata.rb b/db/migrate/20210316171009_create_packages_helm_file_metadata.rb
index f5a9c5f1146..47a9b808e63 100644
--- a/db/migrate/20210316171009_create_packages_helm_file_metadata.rb
+++ b/db/migrate/20210316171009_create_packages_helm_file_metadata.rb
@@ -5,7 +5,7 @@ class CreatePackagesHelmFileMetadata < ActiveRecord::Migration[6.0]
DOWNTIME = false
- def change
+ def up
create_table_with_constraints :packages_helm_file_metadata, id: false do |t|
t.timestamps_with_timezone
t.references :package_file, primary_key: true, index: false, default: nil, null: false, foreign_key: { to_table: :packages_package_files, on_delete: :cascade }, type: :bigint
@@ -17,4 +17,10 @@ class CreatePackagesHelmFileMetadata < ActiveRecord::Migration[6.0]
t.index :channel
end
end
+
+ def down
+ with_lock_retries do
+ drop_table :packages_helm_file_metadata
+ end
+ end
end
diff --git a/db/migrate/20210504153354_create_clusters_integration_elasticstack.rb b/db/migrate/20210504153354_create_clusters_integration_elasticstack.rb
index 5868325e701..79680821060 100644
--- a/db/migrate/20210504153354_create_clusters_integration_elasticstack.rb
+++ b/db/migrate/20210504153354_create_clusters_integration_elasticstack.rb
@@ -3,7 +3,7 @@
class CreateClustersIntegrationElasticstack < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
- def change
+ def up
create_table_with_constraints :clusters_integration_elasticstack, id: false do |t|
t.timestamps_with_timezone null: false
t.references :cluster, primary_key: true, default: nil, index: false, foreign_key: { on_delete: :cascade }
@@ -12,4 +12,10 @@ class CreateClustersIntegrationElasticstack < ActiveRecord::Migration[6.0]
t.text_limit :chart_version, 10
end
end
+
+ def down
+ with_lock_retries do
+ drop_table :clusters_integration_elasticstack
+ end
+ end
end
diff --git a/db/migrate/20210628154900_create_detached_partitions_table.rb b/db/migrate/20210628154900_create_detached_partitions_table.rb
index 05290f4dfb9..cf31d71835a 100644
--- a/db/migrate/20210628154900_create_detached_partitions_table.rb
+++ b/db/migrate/20210628154900_create_detached_partitions_table.rb
@@ -3,7 +3,7 @@
class CreateDetachedPartitionsTable < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
- def change
+ def up
create_table_with_constraints :detached_partitions do |t|
t.timestamps_with_timezone null: false
t.datetime_with_timezone :drop_after, null: false
@@ -14,4 +14,10 @@ class CreateDetachedPartitionsTable < ActiveRecord::Migration[6.1]
t.text_limit :table_name, 63
end
end
+
+ def down
+ with_lock_retries do
+ drop_table :detached_partitions
+ end
+ end
end
diff --git a/db/migrate/20210720140841_create_postgres_async_indexes_table.rb b/db/migrate/20210720140841_create_postgres_async_indexes_table.rb
index 707367e5f6c..99025149840 100644
--- a/db/migrate/20210720140841_create_postgres_async_indexes_table.rb
+++ b/db/migrate/20210720140841_create_postgres_async_indexes_table.rb
@@ -3,7 +3,7 @@
class CreatePostgresAsyncIndexesTable < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
- def change
+ def up
create_table_with_constraints :postgres_async_indexes do |t|
t.timestamps_with_timezone null: false
@@ -18,4 +18,10 @@ class CreatePostgresAsyncIndexesTable < ActiveRecord::Migration[6.1]
t.index :name, unique: true
end
end
+
+ def down
+ with_lock_retries do
+ drop_table :postgres_async_indexes
+ end
+ end
end
diff --git a/db/migrate/20210729081351_create_topics.rb b/db/migrate/20210729081351_create_topics.rb
index c6fdc6bb98a..13ed2dc7ccc 100644
--- a/db/migrate/20210729081351_create_topics.rb
+++ b/db/migrate/20210729081351_create_topics.rb
@@ -3,7 +3,7 @@
class CreateTopics < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
- def change
+ def up
create_table_with_constraints :topics do |t|
t.text :name, null: false
t.text_limit :name, 255
@@ -13,4 +13,10 @@ class CreateTopics < ActiveRecord::Migration[6.1]
t.timestamps_with_timezone
end
end
+
+ def down
+ with_lock_retries do
+ drop_table :topics
+ end
+ end
end
diff --git a/db/post_migrate/20220126201752_remove_projects_ci_job_token_project_scope_links_target_project_id_fk.rb b/db/post_migrate/20220126201752_remove_projects_ci_job_token_project_scope_links_target_project_id_fk.rb
new file mode 100644
index 00000000000..a33e02f2408
--- /dev/null
+++ b/db/post_migrate/20220126201752_remove_projects_ci_job_token_project_scope_links_target_project_id_fk.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class RemoveProjectsCiJobTokenProjectScopeLinksTargetProjectIdFk < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ return unless foreign_key_exists?(:ci_job_token_project_scope_links, :projects, name: "fk_rails_6904b38465")
+
+ with_lock_retries do
+ execute('LOCK projects, ci_job_token_project_scope_links IN ACCESS EXCLUSIVE MODE') if transaction_open?
+
+ remove_foreign_key_if_exists(:ci_job_token_project_scope_links, :projects, name: "fk_rails_6904b38465")
+ end
+ end
+
+ def down
+ add_concurrent_foreign_key(:ci_job_token_project_scope_links, :projects, name: "fk_rails_6904b38465", column: :target_project_id, target_column: :id, on_delete: :cascade)
+ end
+end
diff --git a/db/post_migrate/20220126210021_remove_projects_ci_builds_project_id_fk.rb b/db/post_migrate/20220126210021_remove_projects_ci_builds_project_id_fk.rb
new file mode 100644
index 00000000000..8caa6db7507
--- /dev/null
+++ b/db/post_migrate/20220126210021_remove_projects_ci_builds_project_id_fk.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class RemoveProjectsCiBuildsProjectIdFk < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+
+ def up
+ return if Gitlab.com? # unsafe migration, skip on GitLab.com due to https://gitlab.com/groups/gitlab-org/-/epics/7249#note_819625526
+ return unless foreign_key_exists?(:ci_builds, :projects, name: "fk_befce0568a")
+
+ with_lock_retries do
+ execute('LOCK projects, ci_builds IN ACCESS EXCLUSIVE MODE') if transaction_open?
+
+ remove_foreign_key_if_exists(:ci_builds, :projects, name: "fk_befce0568a")
+ end
+ end
+
+ def down
+ add_concurrent_foreign_key(:ci_builds, :projects, name: "fk_befce0568a", column: :project_id, target_column: :id, on_delete: :cascade)
+ end
+end
diff --git a/db/schema_migrations/20220126201752 b/db/schema_migrations/20220126201752
new file mode 100644
index 00000000000..e5a1970ec9e
--- /dev/null
+++ b/db/schema_migrations/20220126201752
@@ -0,0 +1 @@
+7731772dfac065a60c1626707913ddf6ff632bb69dd5ed6534e8d29e4e03c573 \ No newline at end of file
diff --git a/db/schema_migrations/20220126210021 b/db/schema_migrations/20220126210021
new file mode 100644
index 00000000000..edb9d9c779d
--- /dev/null
+++ b/db/schema_migrations/20220126210021
@@ -0,0 +1 @@
+fd7940bb6f077c91d7f9928f574443ba4bf33bb90cb702c0a2ecad14398ab1cc \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 14da63d4b94..9e40580d3f5 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -29753,9 +29753,6 @@ ALTER TABLE ONLY ci_sources_pipelines
ALTER TABLE ONLY packages_maven_metadata
ADD CONSTRAINT fk_be88aed360 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
-ALTER TABLE ONLY ci_builds
- ADD CONSTRAINT fk_befce0568a FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY design_management_versions
ADD CONSTRAINT fk_c1440b4896 FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL;
@@ -30650,9 +30647,6 @@ ALTER TABLE ONLY resource_iteration_events
ALTER TABLE ONLY geo_hashed_storage_migrated_events
ADD CONSTRAINT fk_rails_687ed7d7c5 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
-ALTER TABLE ONLY ci_job_token_project_scope_links
- ADD CONSTRAINT fk_rails_6904b38465 FOREIGN KEY (target_project_id) REFERENCES projects(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY plan_limits
ADD CONSTRAINT fk_rails_69f8b6184f FOREIGN KEY (plan_id) REFERENCES plans(id) ON DELETE CASCADE;
diff --git a/doc/integration/img/enable_trello_powerup.png b/doc/integration/img/enable_trello_powerup.png
deleted file mode 100644
index f80d0eadc0b..00000000000
--- a/doc/integration/img/enable_trello_powerup.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/trello_power_up.md b/doc/integration/trello_power_up.md
index 96150440e53..8a8952cb594 100644
--- a/doc/integration/trello_power_up.md
+++ b/doc/integration/trello_power_up.md
@@ -6,44 +6,25 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Trello Power-Up **(FREE)**
-The GitLab Trello Power-Up enables you to seamlessly attach
-GitLab **merge requests** to Trello cards.
+You can use the Trello Power-Up for GitLab to attach
+GitLab merge requests to Trello cards.
![GitLab Trello PowerUp - Trello card](img/trello_card_with_gitlab_powerup.png)
-## Configuring the Power-Up
+## Configure the Power-Up
-In order to get started, you must configure your Power-Up.
+To configure a Power-Up for a Trello board:
-In Trello:
+1. Go to your Trello board.
+1. Select **Power-Ups** and find the **GitLab** row.
+1. Select **Enable**.
+1. Select **Settings** (the gear icon).
+1. Select **Authorize Account**.
+1. Enter the [GitLab API URL](#get-the-api-url) and [personal access token](../user/profile/personal_access_tokens.md#create-a-personal-access-token) with the **API** scope.
+1. Select **Save**.
-1. Go to your Trello board
-1. Select `Power-Ups` to see a listing of all the available Power-Ups
-1. Look for a row that says `GitLab` and select the `Enable` button
-1. Select the `Settings` (gear) icon
-1. In the popup menu, select `Authorize Account`
+## Get the API URL
-In this popup, fill in your `API URL` and `Personal Access Token`. After that, you can attach any merge request to any Trello card on your selected Trello board.
-
-## What is my API URL?
-
-Your API URL should be your GitLab instance URL with `/api/v4` appended in the end of the URL.
-For example, if your GitLab instance URL is `https://gitlab.com`, your API URL would be `https://gitlab.com/api/v4`.
-If your instance's URL is `https://example.com`, your API URL is `https://example.com/api/v4`.
-
-![configure GitLab Trello PowerUp in Trello](img/enable_trello_powerup.png)
-
-## What is my Personal Access Token?
-
-Your GitLab personal access token enables your GitLab account to be accessed
-from Trello.
-
-To find it in GitLab:
-
-1. In the top-right corner, select your avatar.
-1. Select **Edit profile**.
-1. On the left sidebar, select **Access Tokens**.
-
-Learn more about generating a personal access token in the
-[Personal Access Token Documentation](../user/profile/personal_access_tokens.md).
-Don't forget to check the API scope checkbox!
+Your API URL is your GitLab instance URL with `/api/v4` appended at the end of the URL.
+For example, if your GitLab instance URL is `https://gitlab.com`, your API URL is `https://gitlab.com/api/v4`.
+If your instance URL is `https://example.com`, your API URL is `https://example.com/api/v4`.
diff --git a/lib/gitlab/database/gitlab_loose_foreign_keys.yml b/lib/gitlab/database/gitlab_loose_foreign_keys.yml
index e7cf19f5e7e..531b9f58a48 100644
--- a/lib/gitlab/database/gitlab_loose_foreign_keys.yml
+++ b/lib/gitlab/database/gitlab_loose_foreign_keys.yml
@@ -42,6 +42,9 @@ ci_job_token_project_scope_links:
- table: projects
column: source_project_id
on_delete: async_delete
+ - table: projects
+ column: target_project_id
+ on_delete: async_delete
ci_daily_build_group_report_results:
- table: namespaces
column: group_id
@@ -88,6 +91,9 @@ ci_builds:
- table: users
column: user_id
on_delete: async_nullify
+ - table: projects
+ column: project_id
+ on_delete: async_delete
ci_pipelines:
- table: merge_requests
column: merge_request_id
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index a2fa2c33a39..23f8a44d2d2 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -429,6 +429,7 @@ module Gitlab
def with_lock_retries(*args, **kwargs, &block)
raise_on_exhaustion = !!kwargs.delete(:raise_on_exhaustion)
merged_args = {
+ connection: connection,
klass: self.class,
logger: Gitlab::BackgroundMigration::Logger,
allow_savepoints: true
diff --git a/lib/gitlab/database/migrations/lock_retry_mixin.rb b/lib/gitlab/database/migrations/lock_retry_mixin.rb
index fff0f35e33c..9774797676a 100644
--- a/lib/gitlab/database/migrations/lock_retry_mixin.rb
+++ b/lib/gitlab/database/migrations/lock_retry_mixin.rb
@@ -9,6 +9,10 @@ module Gitlab
migration.class
end
+ def migration_connection
+ migration.connection
+ end
+
def enable_lock_retries?
# regular AR migrations don't have this,
# only ones inheriting from Gitlab::Database::Migration have
@@ -24,6 +28,7 @@ module Gitlab
def ddl_transaction(migration, &block)
if use_transaction?(migration) && migration.enable_lock_retries?
Gitlab::Database::WithLockRetries.new(
+ connection: migration.migration_connection,
klass: migration.migration_class,
logger: Gitlab::BackgroundMigration::Logger
).run(raise_on_exhaustion: false, &block)
diff --git a/lib/gitlab/database/schema_helpers.rb b/lib/gitlab/database/schema_helpers.rb
index 9ddc5391689..f96de13006f 100644
--- a/lib/gitlab/database/schema_helpers.rb
+++ b/lib/gitlab/database/schema_helpers.rb
@@ -73,6 +73,7 @@ module Gitlab
def with_lock_retries(&block)
Gitlab::Database::WithLockRetries.new(
+ connection: connection,
klass: self.class,
logger: Gitlab::BackgroundMigration::Logger
).run(&block)
diff --git a/lib/gitlab/database/with_lock_retries.rb b/lib/gitlab/database/with_lock_retries.rb
index f9d467ae5cc..f2c5bb9088f 100644
--- a/lib/gitlab/database/with_lock_retries.rb
+++ b/lib/gitlab/database/with_lock_retries.rb
@@ -61,7 +61,7 @@ module Gitlab
[10.seconds, 10.minutes]
].freeze
- def initialize(logger: NULL_LOGGER, allow_savepoints: true, timing_configuration: DEFAULT_TIMING_CONFIGURATION, klass: nil, env: ENV, connection: ActiveRecord::Base.connection)
+ def initialize(logger: NULL_LOGGER, allow_savepoints: true, timing_configuration: DEFAULT_TIMING_CONFIGURATION, klass: nil, env: ENV, connection:)
@logger = logger
@klass = klass
@allow_savepoints = allow_savepoints
diff --git a/lib/gitlab/import_export/json/streaming_serializer.rb b/lib/gitlab/import_export/json/streaming_serializer.rb
index fb8d6e7d89b..d893c8dfaa3 100644
--- a/lib/gitlab/import_export/json/streaming_serializer.rb
+++ b/lib/gitlab/import_export/json/streaming_serializer.rb
@@ -6,15 +6,10 @@ module Gitlab
class StreamingSerializer
include Gitlab::ImportExport::CommandLineUtil
- BATCH_SIZE = 100
- SMALLER_BATCH_SIZE = 2
+ BATCH_SIZE = 2
def self.batch_size(exportable)
- if Feature.enabled?(:export_reduce_relation_batch_size, exportable)
- SMALLER_BATCH_SIZE
- else
- BATCH_SIZE
- end
+ BATCH_SIZE
end
class Raw < String
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 61088c3156d..73eb07d34fc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -27983,10 +27983,13 @@ msgstr ""
msgid "ProjectSettings|Checkbox is visible and unselected by default."
msgstr ""
+msgid "ProjectSettings|Choose the method, options, checks, and squash options for merge requests. You can also set up merge request templates for different actions."
+msgstr ""
+
msgid "ProjectSettings|Choose your merge method, merge options, merge checks, and merge suggestions."
msgstr ""
-msgid "ProjectSettings|Choose your merge method, merge options, merge checks, merge suggestions, and set up a default description template for merge requests."
+msgid "ProjectSettings|Choose your merge method, options, checks, and squash options."
msgstr ""
msgid "ProjectSettings|Configure your project resources and monitor their health."
@@ -33292,6 +33295,9 @@ msgstr ""
msgid "SlackIntegration|GitLab for Slack"
msgstr ""
+msgid "SlackIntegration|GitLab for Slack was successfully installed."
+msgstr ""
+
msgid "SlackIntegration|Project alias"
msgstr ""
@@ -33307,6 +33313,9 @@ msgstr ""
msgid "SlackIntegration|To set up this integration press \"Add to Slack\""
msgstr ""
+msgid "SlackIntegration|You can now close this window and go to your Slack workspace."
+msgstr ""
+
msgid "SlackService|1. %{slash_command_link_start}Add a slash command%{slash_command_link_end} in your Slack team using this information:"
msgstr ""
@@ -34432,6 +34441,27 @@ msgstr ""
msgid "SubscriptionBanner|Upload new license"
msgstr ""
+msgid "SubscriptionEmail|%{doc_link_start}Please reach out if you have questions%{doc_link_end}, and we'll be happy to assist."
+msgstr ""
+
+msgid "SubscriptionEmail|Additional charges for your GitLab subscription"
+msgstr ""
+
+msgid "SubscriptionEmail|Dear %{customer_name},"
+msgstr ""
+
+msgid "SubscriptionEmail|GitLab Billing Team"
+msgstr ""
+
+msgid "SubscriptionEmail|Thank you for your business!"
+msgstr ""
+
+msgid "SubscriptionEmail|You can find more information about the quarterly reconciliation process in %{doc_link_start}our documentation%{doc_link_end}."
+msgstr ""
+
+msgid "SubscriptionEmail|You have exceeded the number of seats in your GitLab subscription %{subscription_name} by %{seat_quantity}. Even if you've exceeded the seats in your subscription, you can continue to add users, and GitLab will bill you a prorated amount for any seat overages on a quarterly basis."
+msgstr ""
+
msgid "SubscriptionTable|Add seats"
msgstr ""
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 59eeb078e9e..26fa5544f3b 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -165,8 +165,8 @@ RSpec.describe GroupDescendantsFinder do
end
context 'with nested groups' do
- let!(:project) { create(:project, namespace: group) }
- let!(:subgroup) { create(:group, :private, parent: group) }
+ let_it_be(:project) { create(:project, namespace: group) }
+ let_it_be_with_reload(:subgroup) { create(:group, :private, parent: group) }
describe '#execute' do
it 'contains projects and subgroups' do
@@ -208,57 +208,69 @@ RSpec.describe GroupDescendantsFinder do
context 'with a filter' do
let(:params) { { filter: 'test' } }
- it 'contains only matching projects and subgroups' do
- matching_project = create(:project, namespace: group, name: 'Testproject')
- matching_subgroup = create(:group, name: 'testgroup', parent: group)
+ shared_examples 'filter examples' do
+ it 'contains only matching projects and subgroups' do
+ matching_project = create(:project, namespace: group, name: 'Testproject')
+ matching_subgroup = create(:group, name: 'testgroup', parent: group)
- expect(finder.execute).to contain_exactly(matching_subgroup, matching_project)
- end
+ expect(finder.execute).to contain_exactly(matching_subgroup, matching_project)
+ end
- it 'does not include subgroups the user does not have access to' do
- _invisible_subgroup = create(:group, :private, parent: group, name: 'test1')
- other_subgroup = create(:group, :private, parent: group, name: 'test2')
- public_subgroup = create(:group, :public, parent: group, name: 'test3')
- other_subsubgroup = create(:group, :private, parent: other_subgroup, name: 'test4')
- other_user = create(:user)
- other_subgroup.add_developer(other_user)
+ it 'does not include subgroups the user does not have access to' do
+ _invisible_subgroup = create(:group, :private, parent: group, name: 'test1')
+ other_subgroup = create(:group, :private, parent: group, name: 'test2')
+ public_subgroup = create(:group, :public, parent: group, name: 'test3')
+ other_subsubgroup = create(:group, :private, parent: other_subgroup, name: 'test4')
+ other_user = create(:user)
+ other_subgroup.add_developer(other_user)
- finder = described_class.new(current_user: other_user,
- parent_group: group,
- params: params)
+ finder = described_class.new(current_user: other_user,
+ parent_group: group,
+ params: params)
- expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup)
- end
+ expect(finder.execute).to contain_exactly(other_subgroup, public_subgroup, other_subsubgroup)
+ end
- context 'with matching children' do
- it 'includes a group that has a subgroup matching the query and its parent' do
- matching_subgroup = create(:group, :private, name: 'testgroup', parent: subgroup)
+ context 'with matching children' do
+ it 'includes a group that has a subgroup matching the query and its parent' do
+ matching_subgroup = create(:group, :private, name: 'testgroup', parent: subgroup)
- expect(finder.execute).to contain_exactly(subgroup, matching_subgroup)
- end
+ expect(finder.execute).to contain_exactly(subgroup, matching_subgroup)
+ end
- it 'includes the parent of a matching project' do
- matching_project = create(:project, namespace: subgroup, name: 'Testproject')
+ it 'includes the parent of a matching project' do
+ matching_project = create(:project, namespace: subgroup, name: 'Testproject')
- expect(finder.execute).to contain_exactly(subgroup, matching_project)
- end
+ expect(finder.execute).to contain_exactly(subgroup, matching_project)
+ end
+
+ context 'with a small page size' do
+ let(:params) { { filter: 'test', per_page: 1 } }
+
+ it 'contains all the ancestors of a matching subgroup regardless the page size' do
+ subgroup = create(:group, :private, parent: group)
+ matching = create(:group, :private, name: 'testgroup', parent: subgroup)
- context 'with a small page size' do
- let(:params) { { filter: 'test', per_page: 1 } }
+ expect(finder.execute).to contain_exactly(subgroup, matching)
+ end
+ end
- it 'contains all the ancestors of a matching subgroup regardless the page size' do
- subgroup = create(:group, :private, parent: group)
- matching = create(:group, :private, name: 'testgroup', parent: subgroup)
+ it 'does not include the parent itself' do
+ group.update!(name: 'test')
- expect(finder.execute).to contain_exactly(subgroup, matching)
+ expect(finder.execute).not_to include(group)
end
end
+ end
- it 'does not include the parent itself' do
- group.update!(name: 'test')
+ it_behaves_like 'filter examples'
- expect(finder.execute).not_to include(group)
+ context 'when feature flag :linear_group_descendants_finder_upto is disabled' do
+ before do
+ stub_feature_flags(linear_group_descendants_finder_upto: false)
end
+
+ it_behaves_like 'filter examples'
end
end
end
diff --git a/spec/frontend/environments/deployment_spec.js b/spec/frontend/environments/deployment_spec.js
index a1b232d783e..24992711a4a 100644
--- a/spec/frontend/environments/deployment_spec.js
+++ b/spec/frontend/environments/deployment_spec.js
@@ -1,7 +1,9 @@
+import { GlCollapse } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { __, s__ } from '~/locale';
-import { formatDate } from '~/lib/utils/datetime_utility';
import { useFakeDate } from 'helpers/fake_date';
+import { stubTransition } from 'helpers/stub_transition';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { __, s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Deployment from '~/environments/components/deployment.vue';
import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
@@ -19,6 +21,7 @@ describe('~/environments/components/deployment.vue', () => {
deployment,
...propsData,
},
+ stubs: { transition: stubTransition() },
});
afterEach(() => {
@@ -148,4 +151,29 @@ describe('~/environments/components/deployment.vue', () => {
});
});
});
+
+ describe('collapse', () => {
+ let collapse;
+ let button;
+
+ beforeEach(() => {
+ wrapper = createWrapper();
+ collapse = wrapper.findComponent(GlCollapse);
+ button = wrapper.findComponent({ ref: 'details-toggle' });
+ });
+
+ it('is collapsed by default', () => {
+ expect(collapse.attributes('visible')).toBeUndefined();
+ expect(button.props('icon')).toBe('expand-down');
+ expect(button.text()).toBe(__('Show details'));
+ });
+
+ it('opens on click', async () => {
+ await button.trigger('click');
+
+ expect(button.text()).toBe(__('Hide details'));
+ expect(button.props('icon')).toBe('expand-up');
+ expect(collapse.attributes('visible')).toBe('visible');
+ });
+ });
});
diff --git a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
index 61a4f7a443c..9a02e9057c2 100644
--- a/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -8,6 +8,8 @@ import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/runner/components/runner_header.vue';
import RunnerDetails from '~/runner/components/runner_details.vue';
+import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
+import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import getRunnerQuery from '~/runner/graphql/get_runner.query.graphql';
import AdminRunnerShowApp from '~/runner/admin_runner_show/admin_runner_show_app.vue';
import { captureException } from '~/runner/sentry_utils';
@@ -17,7 +19,8 @@ import { runnerData } from '../mock_data';
jest.mock('~/flash');
jest.mock('~/runner/sentry_utils');
-const mockRunnerGraphqlId = runnerData.data.runner.id;
+const mockRunner = runnerData.data.runner;
+const mockRunnerGraphqlId = mockRunner.id;
const mockRunnerId = `${getIdFromGraphQLId(mockRunnerGraphqlId)}`;
Vue.use(VueApollo);
@@ -28,6 +31,16 @@ describe('AdminRunnerShowApp', () => {
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
+ const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
+ const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+
+ const mockRunnerQueryResult = (runner = {}) => {
+ mockRunnerQuery = jest.fn().mockResolvedValue({
+ data: {
+ runner: { ...mockRunner, ...runner },
+ },
+ });
+ };
const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(AdminRunnerShowApp, {
@@ -41,10 +54,6 @@ describe('AdminRunnerShowApp', () => {
return waitForPromises();
};
- beforeEach(() => {
- mockRunnerQuery = jest.fn().mockResolvedValue(runnerData);
- });
-
afterEach(() => {
mockRunnerQuery.mockReset();
wrapper.destroy();
@@ -52,6 +61,8 @@ describe('AdminRunnerShowApp', () => {
describe('When showing runner details', () => {
beforeEach(async () => {
+ mockRunnerQueryResult();
+
await createComponent({ mountFn: mount });
});
@@ -63,6 +74,11 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
+ it('displays the runner edit and pause buttons', async () => {
+ expect(findRunnerEditButton().exists()).toBe(true);
+ expect(findRunnerPauseButton().exists()).toBe(true);
+ });
+
it('shows basic runner details', async () => {
const expected = `Details
Description Instance runner
@@ -75,6 +91,42 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerDetails().text()).toMatchInterpolatedText(expected);
});
+
+ describe('when runner cannot be updated', () => {
+ beforeEach(async () => {
+ mockRunnerQueryResult({
+ userPermissions: {
+ updateRunner: false,
+ },
+ });
+
+ await createComponent({
+ mountFn: mount,
+ });
+ });
+
+ it('does not display the runner edit and pause buttons', () => {
+ expect(findRunnerEditButton().exists()).toBe(false);
+ expect(findRunnerPauseButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when runner does not have an edit url ', () => {
+ beforeEach(async () => {
+ mockRunnerQueryResult({
+ editAdminUrl: null,
+ });
+
+ await createComponent({
+ mountFn: mount,
+ });
+ });
+
+ it('does not display the runner edit button', () => {
+ expect(findRunnerEditButton().exists()).toBe(false);
+ expect(findRunnerPauseButton().exists()).toBe(true);
+ });
+ });
});
describe('When there is an error', () => {
diff --git a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
index 69838ab95fa..b2c740c3f27 100644
--- a/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
+++ b/spec/frontend/runner/components/cells/runner_actions_cell_spec.js
@@ -9,12 +9,12 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { captureException } from '~/runner/sentry_utils';
import RunnerActionCell from '~/runner/components/cells/runner_actions_cell.vue';
+import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
import RunnerDeleteModal from '~/runner/components/runner_delete_modal.vue';
import getGroupRunnersQuery from '~/runner/graphql/get_group_runners.query.graphql';
import getRunnersQuery from '~/runner/graphql/get_runners.query.graphql';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
-import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
import { runnersData } from '../../mock_data';
const mockRunner = runnersData.data.runners.nodes[0];
@@ -32,10 +32,9 @@ describe('RunnerTypeCell', () => {
const mockToastShow = jest.fn();
const runnerDeleteMutationHandler = jest.fn();
- const runnerActionsUpdateMutationHandler = jest.fn();
const findEditBtn = () => wrapper.findComponent(RunnerEditButton);
- const findToggleActiveBtn = () => wrapper.findByTestId('toggle-active-runner');
+ const findRunnerPauseBtn = () => wrapper.findComponent(RunnerPauseButton);
const findRunnerDeleteModal = () => wrapper.findComponent(RunnerDeleteModal);
const findDeleteBtn = () => wrapper.findByTestId('delete-runner');
const getTooltip = (w) => getBinding(w.element, 'gl-tooltip')?.value;
@@ -52,10 +51,7 @@ describe('RunnerTypeCell', () => {
...runner,
},
},
- apolloProvider: createMockApollo([
- [runnerDeleteMutation, runnerDeleteMutationHandler],
- [runnerActionsUpdateMutation, runnerActionsUpdateMutationHandler],
- ]),
+ apolloProvider: createMockApollo([[runnerDeleteMutation, runnerDeleteMutationHandler]]),
directives: {
GlTooltip: createMockDirective(),
GlModal: createMockDirective(),
@@ -77,21 +73,11 @@ describe('RunnerTypeCell', () => {
},
},
});
-
- runnerActionsUpdateMutationHandler.mockResolvedValue({
- data: {
- runnerUpdate: {
- runner: mockRunner,
- errors: [],
- },
- },
- });
});
afterEach(() => {
mockToastShow.mockReset();
runnerDeleteMutationHandler.mockReset();
- runnerActionsUpdateMutationHandler.mockReset();
wrapper.destroy();
});
@@ -123,118 +109,14 @@ describe('RunnerTypeCell', () => {
});
});
- describe('Toggle active action', () => {
- describe.each`
- state | label | icon | isActive | newActiveValue
- ${'active'} | ${'Pause'} | ${'pause'} | ${true} | ${false}
- ${'paused'} | ${'Resume'} | ${'play'} | ${false} | ${true}
- `('When the runner is $state', ({ label, icon, isActive, newActiveValue }) => {
- beforeEach(() => {
- createComponent({ active: isActive });
- });
-
- it(`Displays a ${icon} button`, () => {
- expect(findToggleActiveBtn().props('loading')).toBe(false);
- expect(findToggleActiveBtn().props('icon')).toBe(icon);
- expect(getTooltip(findToggleActiveBtn())).toBe(label);
- expect(findToggleActiveBtn().attributes('aria-label')).toBe(label);
- });
-
- it(`After clicking the ${icon} button, the button has a loading state`, async () => {
- await findToggleActiveBtn().vm.$emit('click');
-
- expect(findToggleActiveBtn().props('loading')).toBe(true);
- });
-
- it(`After the ${icon} button is clicked, stale tooltip is removed`, async () => {
- await findToggleActiveBtn().vm.$emit('click');
-
- expect(getTooltip(findToggleActiveBtn())).toBe('');
- expect(findToggleActiveBtn().attributes('aria-label')).toBe('');
- });
-
- describe(`When clicking on the ${icon} button`, () => {
- it(`The apollo mutation to set active to ${newActiveValue} is called`, async () => {
- expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(0);
-
- await findToggleActiveBtn().vm.$emit('click');
-
- expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledTimes(1);
- expect(runnerActionsUpdateMutationHandler).toHaveBeenCalledWith({
- input: {
- id: mockRunner.id,
- active: newActiveValue,
- },
- });
- });
-
- it('The button does not have a loading state after the mutation occurs', async () => {
- await findToggleActiveBtn().vm.$emit('click');
-
- expect(findToggleActiveBtn().props('loading')).toBe(true);
-
- await waitForPromises();
-
- expect(findToggleActiveBtn().props('loading')).toBe(false);
- });
- });
-
- describe('When update fails', () => {
- describe('On a network error', () => {
- const mockErrorMsg = 'Update error!';
-
- beforeEach(async () => {
- runnerActionsUpdateMutationHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
-
- findToggleActiveBtn().vm.$emit('click');
- await waitForPromises();
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(`Network error: ${mockErrorMsg}`),
- component: 'RunnerActionsCell',
- });
- });
-
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('On a validation error', () => {
- const mockErrorMsg = 'Runner not found!';
- const mockErrorMsg2 = 'User not allowed!';
-
- beforeEach(async () => {
- runnerActionsUpdateMutationHandler.mockResolvedValue({
- data: {
- runnerUpdate: {
- runner: mockRunner,
- errors: [mockErrorMsg, mockErrorMsg2],
- },
- },
- });
-
- findToggleActiveBtn().vm.$emit('click');
- await waitForPromises();
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
- component: 'RunnerActionsCell',
- });
- });
+ describe('Pause action', () => {
+ it('Renders a compact pause button', () => {
+ createComponent();
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
- });
- });
+ expect(findRunnerPauseBtn().props('compact')).toBe(true);
});
- it('Does not render the runner toggle active button when user cannot update', () => {
+ it('Does not render the runner pause button when user cannot update', () => {
createComponent({
userPermissions: {
...mockRunner.userPermissions,
@@ -242,7 +124,7 @@ describe('RunnerTypeCell', () => {
},
});
- expect(findToggleActiveBtn().exists()).toBe(false);
+ expect(findRunnerPauseBtn().exists()).toBe(false);
});
});
diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js
index 13f53c36785..c50486b6f7b 100644
--- a/spec/frontend/runner/components/runner_list_spec.js
+++ b/spec/frontend/runner/components/runner_list_spec.js
@@ -7,6 +7,7 @@ import {
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerList from '~/runner/components/runner_list.vue';
import RunnerEditButton from '~/runner/components/runner_edit_button.vue';
+import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
import { runnersData } from '../mock_data';
const mockRunners = runnersData.data.runners.nodes;
@@ -92,7 +93,8 @@ describe('RunnerList', () => {
const actions = findCell({ fieldKey: 'actions' });
expect(actions.findComponent(RunnerEditButton).exists()).toBe(true);
- expect(actions.findByTestId('toggle-active-runner').exists()).toBe(true);
+ expect(actions.findComponent(RunnerPauseButton).exists()).toBe(true);
+ expect(actions.findByTestId('delete-runner').exists()).toBe(true);
});
describe('Table data formatting', () => {
diff --git a/spec/frontend/runner/components/runner_pause_button_spec.js b/spec/frontend/runner/components/runner_pause_button_spec.js
new file mode 100644
index 00000000000..34510883a25
--- /dev/null
+++ b/spec/frontend/runner/components/runner_pause_button_spec.js
@@ -0,0 +1,239 @@
+import Vue from 'vue';
+import { GlButton } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import runnerToggleActiveMutation from '~/runner/graphql/runner_toggle_active.mutation.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
+import { captureException } from '~/runner/sentry_utils';
+import { createAlert } from '~/flash';
+
+import RunnerPauseButton from '~/runner/components/runner_pause_button.vue';
+import { runnersData } from '../mock_data';
+
+const mockRunner = runnersData.data.runners.nodes[0];
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
+
+describe('RunnerPauseButton', () => {
+ let wrapper;
+ let runnerToggleActiveHandler;
+
+ const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
+ const findBtn = () => wrapper.findComponent(GlButton);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ const { runner, ...propsData } = props;
+
+ wrapper = mountFn(RunnerPauseButton, {
+ propsData: {
+ runner: {
+ id: mockRunner.id,
+ active: mockRunner.active,
+ ...runner,
+ },
+ ...propsData,
+ },
+ apolloProvider: createMockApollo([[runnerToggleActiveMutation, runnerToggleActiveHandler]]),
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ const clickAndWait = async () => {
+ findBtn().vm.$emit('click');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ runnerToggleActiveHandler = jest.fn().mockImplementation(({ input }) => {
+ return Promise.resolve({
+ data: {
+ runnerUpdate: {
+ runner: {
+ id: input.id,
+ active: input.active,
+ },
+ errors: [],
+ },
+ },
+ });
+ });
+
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('Pause/Resume action', () => {
+ describe.each`
+ runnerState | icon | content | isActive | newActiveValue
+ ${'paused'} | ${'play'} | ${'Resume'} | ${false} | ${true}
+ ${'active'} | ${'pause'} | ${'Pause'} | ${true} | ${false}
+ `('When the runner is $runnerState', ({ icon, content, isActive, newActiveValue }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ active: isActive,
+ },
+ },
+ });
+ });
+
+ it(`Displays a ${icon} button`, () => {
+ expect(findBtn().props('loading')).toBe(false);
+ expect(findBtn().props('icon')).toBe(icon);
+ expect(findBtn().text()).toBe(content);
+ });
+
+ it('Does not display redundant text for screen readers', () => {
+ expect(findBtn().attributes('aria-label')).toBe(undefined);
+ });
+
+ describe(`Before the ${icon} button is clicked`, () => {
+ it('The mutation has not been called', () => {
+ expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe(`Immediately after the ${icon} button is clicked`, () => {
+ beforeEach(async () => {
+ findBtn().vm.$emit('click');
+ });
+
+ it('The button has a loading state', async () => {
+ expect(findBtn().props('loading')).toBe(true);
+ });
+
+ it('The stale tooltip is removed', async () => {
+ expect(getTooltip()).toBe('');
+ });
+ });
+
+ describe(`After clicking on the ${icon} button`, () => {
+ beforeEach(async () => {
+ await clickAndWait();
+ });
+
+ it(`The mutation to that sets active to ${newActiveValue} is called`, async () => {
+ expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(1);
+ expect(runnerToggleActiveHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockRunner.id,
+ active: newActiveValue,
+ },
+ });
+ });
+
+ it('The button does not have a loading state', () => {
+ expect(findBtn().props('loading')).toBe(false);
+ });
+ });
+
+ describe('When update fails', () => {
+ describe('On a network error', () => {
+ const mockErrorMsg = 'Update error!';
+
+ beforeEach(async () => {
+ runnerToggleActiveHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+
+ await clickAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`Network error: ${mockErrorMsg}`),
+ component: 'RunnerPauseButton',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('On a validation error', () => {
+ const mockErrorMsg = 'Runner not found!';
+ const mockErrorMsg2 = 'User not allowed!';
+
+ beforeEach(async () => {
+ runnerToggleActiveHandler.mockResolvedValueOnce({
+ data: {
+ runnerUpdate: {
+ runner: {
+ id: mockRunner.id,
+ active: isActive,
+ },
+ errors: [mockErrorMsg, mockErrorMsg2],
+ },
+ },
+ });
+
+ await clickAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
+ component: 'RunnerPauseButton',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+ });
+ });
+
+ describe('When displaying a compact button for an active runner', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ active: true,
+ },
+ compact: true,
+ },
+ mountFn: mountExtended,
+ });
+ });
+
+ it('Displays no text', () => {
+ expect(findBtn().text()).toBe('');
+
+ // Note: Use <template v-if> to ensure rendering a
+ // text-less button. Ensure we don't send even empty an
+ // content slot to prevent a distorted/rectangular button.
+ expect(wrapper.find('.gl-button-text').exists()).toBe(false);
+ });
+
+ it('Display correctly for screen readers', () => {
+ expect(findBtn().attributes('aria-label')).toBe('Pause');
+ expect(getTooltip()).toBe('Pause');
+ });
+
+ describe('Immediately after the button is clicked', () => {
+ beforeEach(async () => {
+ findBtn().vm.$emit('click');
+ });
+
+ it('The button has a loading state', async () => {
+ expect(findBtn().props('loading')).toBe(true);
+ });
+
+ it('The stale tooltip is removed', async () => {
+ expect(getTooltip()).toBe('');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js
index cbdf7f53913..963577fa763 100644
--- a/spec/frontend/security_configuration/components/app_spec.js
+++ b/spec/frontend/security_configuration/components/app_spec.js
@@ -32,7 +32,7 @@ const upgradePath = '/upgrade';
const autoDevopsHelpPagePath = '/autoDevopsHelpPagePath';
const autoDevopsPath = '/autoDevopsPath';
const gitlabCiHistoryPath = 'test/historyPath';
-const projectPath = 'namespace/project';
+const projectFullPath = 'namespace/project';
useLocalStorageSpy();
@@ -54,7 +54,7 @@ describe('App component', () => {
upgradePath,
autoDevopsHelpPagePath,
autoDevopsPath,
- projectPath,
+ projectFullPath,
glFeatures: {
secureVulnerabilityTraining,
},
@@ -274,11 +274,11 @@ describe('App component', () => {
describe('Auto DevOps enabled alert', () => {
describe.each`
- context | autoDevopsEnabled | localStorageValue | shouldRender
- ${'enabled'} | ${true} | ${null} | ${true}
- ${'enabled, alert dismissed on other project'} | ${true} | ${['foo/bar']} | ${true}
- ${'enabled, alert dismissed on this project'} | ${true} | ${[projectPath]} | ${false}
- ${'not enabled'} | ${false} | ${null} | ${false}
+ context | autoDevopsEnabled | localStorageValue | shouldRender
+ ${'enabled'} | ${true} | ${null} | ${true}
+ ${'enabled, alert dismissed on other project'} | ${true} | ${['foo/bar']} | ${true}
+ ${'enabled, alert dismissed on this project'} | ${true} | ${[projectFullPath]} | ${false}
+ ${'not enabled'} | ${false} | ${null} | ${false}
`('given Auto DevOps is $context', ({ autoDevopsEnabled, localStorageValue, shouldRender }) => {
beforeEach(() => {
if (localStorageValue !== null) {
@@ -302,11 +302,11 @@ describe('App component', () => {
describe('dismissing', () => {
describe.each`
- dismissedProjects | expectedWrittenValue
- ${null} | ${[projectPath]}
- ${[]} | ${[projectPath]}
- ${['foo/bar']} | ${['foo/bar', projectPath]}
- ${[projectPath]} | ${[projectPath]}
+ dismissedProjects | expectedWrittenValue
+ ${null} | ${[projectFullPath]}
+ ${[]} | ${[projectFullPath]}
+ ${['foo/bar']} | ${['foo/bar', projectFullPath]}
+ ${[projectFullPath]} | ${[projectFullPath]}
`(
'given dismissed projects $dismissedProjects',
({ dismissedProjects, expectedWrittenValue }) => {
diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js
index 578248e696f..f225c135d49 100644
--- a/spec/frontend/security_configuration/components/training_provider_list_spec.js
+++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js
@@ -4,11 +4,12 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
+import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import {
securityTrainingProviders,
- createMockResolvers,
+ securityTrainingProvidersResponse,
testProjectPath,
textProviderIds,
} from '../mock_data';
@@ -19,14 +20,19 @@ describe('TrainingProviderList component', () => {
let wrapper;
let apolloProvider;
- const createApolloProvider = ({ resolvers } = {}) => {
- apolloProvider = createMockApollo([], createMockResolvers({ resolvers }));
+ const createApolloProvider = ({ resolvers, queryHandler } = {}) => {
+ const defaultQueryHandler = jest.fn().mockResolvedValue(securityTrainingProvidersResponse);
+
+ apolloProvider = createMockApollo(
+ [[securityTrainingProvidersQuery, queryHandler || defaultQueryHandler]],
+ resolvers,
+ );
};
const createComponent = () => {
wrapper = shallowMount(TrainingProviderList, {
provide: {
- projectPath: testProjectPath,
+ projectFullPath: testProjectPath,
},
apolloProvider,
});
@@ -49,20 +55,29 @@ describe('TrainingProviderList component', () => {
apolloProvider = null;
});
- describe('with a successful response', () => {
+ describe('when loading', () => {
beforeEach(() => {
- createApolloProvider();
+ const pendingHandler = () => new Promise(() => {});
+
+ createApolloProvider({
+ queryHandler: pendingHandler,
+ });
createComponent();
});
- describe('when loading', () => {
- it('shows the loader', () => {
- expect(findLoader().exists()).toBe(true);
- });
+ it('shows the loader', () => {
+ expect(findLoader().exists()).toBe(true);
+ });
- it('does not show the cards', () => {
- expect(findCards().exists()).toBe(false);
- });
+ it('does not show the cards', () => {
+ expect(findCards().exists()).toBe(false);
+ });
+ });
+
+ describe('with a successful response', () => {
+ beforeEach(() => {
+ createApolloProvider();
+ createComponent();
});
describe('basic structure', () => {
@@ -142,11 +157,7 @@ describe('TrainingProviderList component', () => {
describe('when fetching training providers', () => {
beforeEach(async () => {
createApolloProvider({
- resolvers: {
- Query: {
- securityTrainingProviders: jest.fn().mockReturnValue(new Error()),
- },
- },
+ queryHandler: jest.fn().mockReturnValue(new Error()),
});
createComponent();
diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js
index 37ecce3886d..07c292020a3 100644
--- a/spec/frontend/security_configuration/mock_data.js
+++ b/spec/frontend/security_configuration/mock_data.js
@@ -21,19 +21,9 @@ export const securityTrainingProviders = [
export const securityTrainingProvidersResponse = {
data: {
- securityTrainingProviders,
- },
-};
-
-const defaultMockResolvers = {
- Query: {
- securityTrainingProviders() {
- return securityTrainingProviders;
+ project: {
+ id: 1,
+ securityTrainingProviders,
},
},
};
-
-export const createMockResolvers = ({ resolvers: customMockResolvers = {} } = {}) => ({
- ...defaultMockResolvers,
- ...customMockResolvers,
-});
diff --git a/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js
index 15eef79d425..a9fe29a484a 100644
--- a/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js
+++ b/spec/frontend/vue_mr_widget/extentions/accessibility/index_spec.js
@@ -95,10 +95,11 @@ describe('Accessibility extension', () => {
await waitForPromises();
- findToggleCollapsedButton().vm.$emit('click');
+ findToggleCollapsedButton().trigger('click');
await waitForPromises();
});
+
it('displays all report list items', async () => {
expect(findAllExtensionListItems()).toHaveLength(10);
});
diff --git a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
index facbd51168c..39909e26ef0 100644
--- a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
+++ b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js
@@ -16,7 +16,7 @@ jest.mock('~/lib/utils/url_utility');
Vue.use(VueApollo);
-const projectPath = 'namespace/project';
+const projectFullPath = 'namespace/project';
describe('ManageViaMr component', () => {
let wrapper;
@@ -40,7 +40,7 @@ describe('ManageViaMr component', () => {
wrapper = extendedWrapper(
mount(ManageViaMr, {
provide: {
- projectPath,
+ projectFullPath,
},
propsData: {
feature: {
@@ -65,7 +65,7 @@ describe('ManageViaMr component', () => {
// the ones available in the current test context.
const supportedReportTypes = Object.entries(featureToMutationMap).map(
([featureType, { getMutationPayload, mutationId }]) => {
- const { mutation, variables: mutationVariables } = getMutationPayload(projectPath);
+ const { mutation, variables: mutationVariables } = getMutationPayload(projectFullPath);
return [humanize(featureType), featureType, mutation, mutationId, mutationVariables];
},
);
diff --git a/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb b/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb
index 076fb9e8215..50ad77caaf1 100644
--- a/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb
+++ b/spec/lib/gitlab/database/migrations/lock_retry_mixin_spec.rb
@@ -3,7 +3,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do
describe Gitlab::Database::Migrations::LockRetryMixin::ActiveRecordMigrationProxyLockRetries do
- let(:migration) { double }
+ let(:connection) { ActiveRecord::Base.connection }
+ let(:migration) { double(connection: connection) }
let(:return_value) { double }
let(:class_def) do
Class.new do
@@ -40,6 +41,18 @@ RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do
expect(result).to eq(return_value)
end
end
+
+ describe '#migration_connection' do
+ subject { class_def.new(migration).migration_connection }
+
+ it 'retrieves actual migration connection from #migration' do
+ expect(migration).to receive(:connection).and_return(return_value)
+
+ result = subject
+
+ expect(result).to eq(return_value)
+ end
+ end
end
describe Gitlab::Database::Migrations::LockRetryMixin::ActiveRecordMigratorLockRetries do
@@ -96,7 +109,8 @@ RSpec.describe Gitlab::Database::Migrations::LockRetryMixin do
context 'with transactions enabled and lock retries enabled' do
let(:receiver) { double('receiver', use_transaction?: true)}
- let(:migration) { double('migration', enable_lock_retries?: true) }
+ let(:migration) { double('migration', migration_connection: connection, enable_lock_retries?: true) }
+ let(:connection) { ActiveRecord::Base.connection }
it 'calls super method' do
p = proc { }
diff --git a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
index f815baa7d1d..ab42545e951 100644
--- a/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
+++ b/spec/lib/gitlab/database/no_cross_db_foreign_keys_spec.rb
@@ -12,13 +12,11 @@ RSpec.describe 'cross-database foreign keys' do
let(:allowed_cross_database_foreign_keys) do
%w(
ci_build_report_results.project_id
- ci_builds.project_id
ci_daily_build_group_report_results.group_id
ci_daily_build_group_report_results.project_id
ci_freeze_periods.project_id
ci_job_artifacts.project_id
ci_job_token_project_scope_links.added_by_id
- ci_job_token_project_scope_links.target_project_id
ci_pending_builds.namespace_id
ci_pending_builds.project_id
ci_pipeline_schedules.owner_id
diff --git a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
index 0282a7af0df..6c32fb3ca17 100644
--- a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb
@@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
let(:env) { {} }
let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER }
- let(:subject) { described_class.new(env: env, logger: logger, timing_configuration: timing_configuration) }
+ let(:subject) { described_class.new(connection: connection, env: env, logger: logger, timing_configuration: timing_configuration) }
+ let(:connection) { ActiveRecord::Base.retrieve_connection }
let(:timing_configuration) do
[
@@ -67,7 +68,7 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
WHERE t.relkind = 'r' AND l.mode = 'ExclusiveLock' AND t.relname = '#{Project.table_name}'
"""
- expect(ActiveRecord::Base.connection.execute(check_exclusive_lock_query).to_a).to be_present
+ expect(connection.execute(check_exclusive_lock_query).to_a).to be_present
end
end
@@ -96,8 +97,8 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
lock_fiber.resume
end
- ActiveRecord::Base.transaction do
- ActiveRecord::Base.connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
+ connection.transaction do
+ connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
lock_acquired = true
end
end
@@ -115,7 +116,7 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
context 'setting the idle transaction timeout' do
context 'when there is no outer transaction: disable_ddl_transaction! is set in the migration' do
it 'does not disable the idle transaction timeout' do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(false)
+ allow(connection).to receive(:transaction_open?).and_return(false)
allow(subject).to receive(:run_block_with_lock_timeout).once.and_raise(ActiveRecord::LockWaitTimeout)
allow(subject).to receive(:run_block_with_lock_timeout).once
@@ -127,7 +128,7 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
context 'when there is outer transaction: disable_ddl_transaction! is not set in the migration' do
it 'disables the idle transaction timeout so the code can sleep and retry' do
- allow(ActiveRecord::Base.connection).to receive(:transaction_open?).and_return(true)
+ allow(connection).to receive(:transaction_open?).and_return(true)
n = 0
allow(subject).to receive(:run_block_with_lock_timeout).twice do
@@ -184,8 +185,8 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
subject.run(raise_on_exhaustion: true) do
lock_attempts += 1
- ActiveRecord::Base.transaction do
- ActiveRecord::Base.connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
+ connection.transaction do
+ connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
lock_acquired = true
end
end
@@ -199,11 +200,11 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
context 'when statement timeout is reached' do
it 'raises StatementInvalid error' do
lock_acquired = false
- ActiveRecord::Base.connection.execute("SET statement_timeout='100ms'")
+ connection.execute("SET statement_timeout='100ms'")
expect do
subject.run do
- ActiveRecord::Base.connection.execute("SELECT 1 FROM pg_sleep(0.11)") # 110ms
+ connection.execute("SELECT 1 FROM pg_sleep(0.11)") # 110ms
lock_acquired = true
end
end.to raise_error(ActiveRecord::StatementInvalid)
@@ -216,11 +217,11 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
context 'restore local database variables' do
it do
- expect { subject.run {} }.not_to change { ActiveRecord::Base.connection.execute("SHOW lock_timeout").to_a }
+ expect { subject.run {} }.not_to change { connection.execute("SHOW lock_timeout").to_a }
end
it do
- expect { subject.run {} }.not_to change { ActiveRecord::Base.connection.execute("SHOW idle_in_transaction_session_timeout").to_a }
+ expect { subject.run {} }.not_to change { connection.execute("SHOW idle_in_transaction_session_timeout").to_a }
end
end
@@ -228,8 +229,8 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction do
let(:timing_configuration) { [[0.015.seconds, 0.025.seconds], [0.015.seconds, 0.025.seconds]] } # 15ms, 25ms
it 'executes `SET lock_timeout` using the configured timeout value in milliseconds' do
- expect(ActiveRecord::Base.connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout; RESET lock_timeout').and_call_original
- expect(ActiveRecord::Base.connection).to receive(:execute).with("SET lock_timeout TO '15ms'").and_call_original
+ expect(connection).to receive(:execute).with('RESET idle_in_transaction_session_timeout; RESET lock_timeout').and_call_original
+ expect(connection).to receive(:execute).with("SET lock_timeout TO '15ms'").and_call_original
subject.run { }
end
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index c2c818aa106..6b35ccafabc 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::WithLockRetries do
let(:env) { {} }
let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER }
- let(:subject) { described_class.new(env: env, logger: logger, allow_savepoints: allow_savepoints, timing_configuration: timing_configuration) }
+ let(:subject) { described_class.new(connection: connection, env: env, logger: logger, allow_savepoints: allow_savepoints, timing_configuration: timing_configuration) }
let(:allow_savepoints) { true }
let(:connection) { ActiveRecord::Base.retrieve_connection }
diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
index d69d775fffb..352af18c822 100644
--- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
+++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb
@@ -183,24 +183,8 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer do
end
describe '.batch_size' do
- context 'when export_reduce_relation_batch_size feature flag is enabled' do
- before do
- stub_feature_flags(export_reduce_relation_batch_size: true)
- end
-
- it 'returns 20' do
- expect(described_class.batch_size(exportable)).to eq(described_class::SMALLER_BATCH_SIZE)
- end
- end
-
- context 'when export_reduce_relation_batch_size feature flag is disabled' do
- before do
- stub_feature_flags(export_reduce_relation_batch_size: false)
- end
-
- it 'returns default batch size' do
- expect(described_class.batch_size(exportable)).to eq(described_class::BATCH_SIZE)
- end
+ it 'returns default batch size' do
+ expect(described_class.batch_size(exportable)).to eq(described_class::BATCH_SIZE)
end
end
end
diff --git a/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb b/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
index 3b7ed7cb32b..0d372def8b0 100644
--- a/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/legacy_relation_tree_saver_spec.rb
@@ -8,35 +8,17 @@ RSpec.describe Gitlab::ImportExport::LegacyRelationTreeSaver do
let(:tree) { {} }
describe '#serialize' do
- shared_examples 'FastHashSerializer with batch size' do |batch_size|
- let(:serializer) { instance_double(Gitlab::ImportExport::FastHashSerializer) }
+ let(:serializer) { instance_double(Gitlab::ImportExport::FastHashSerializer) }
- it 'uses FastHashSerializer' do
- expect(Gitlab::ImportExport::FastHashSerializer)
- .to receive(:new)
- .with(exportable, tree, batch_size: batch_size)
- .and_return(serializer)
+ it 'uses FastHashSerializer' do
+ expect(Gitlab::ImportExport::FastHashSerializer)
+ .to receive(:new)
+ .with(exportable, tree, batch_size: Gitlab::ImportExport::Json::StreamingSerializer::BATCH_SIZE)
+ .and_return(serializer)
- expect(serializer).to receive(:execute)
+ expect(serializer).to receive(:execute)
- relation_tree_saver.serialize(exportable, tree)
- end
- end
-
- context 'when export_reduce_relation_batch_size feature flag is enabled' do
- before do
- stub_feature_flags(export_reduce_relation_batch_size: true)
- end
-
- include_examples 'FastHashSerializer with batch size', Gitlab::ImportExport::Json::StreamingSerializer::SMALLER_BATCH_SIZE
- end
-
- context 'when export_reduce_relation_batch_size feature flag is disabled' do
- before do
- stub_feature_flags(export_reduce_relation_batch_size: false)
- end
-
- include_examples 'FastHashSerializer with batch size', Gitlab::ImportExport::Json::StreamingSerializer::BATCH_SIZE
+ relation_tree_saver.serialize(exportable, tree)
end
end
end
diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb
index 8494446476f..c000a3e29f7 100644
--- a/spec/models/ci/job_token/project_scope_link_spec.rb
+++ b/spec/models/ci/job_token/project_scope_link_spec.rb
@@ -95,4 +95,11 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do
let!(:model) { create(:ci_job_token_project_scope_link, source_project: parent) }
end
end
+
+ context 'loose foreign key on ci_job_token_project_scope_links.target_project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_job_token_project_scope_link, target_project: parent) }
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index d5e74d36b58..86ee159b97e 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -987,4 +987,11 @@ RSpec.describe CommitStatus do
commit_status.expire_etag_cache!
end
end
+
+ context 'loose foreign key on ci_builds.project_id' do
+ it_behaves_like 'cleanup by a loose foreign key' do
+ let!(:parent) { create(:project) }
+ let!(:model) { create(:ci_build, project: parent) }
+ end
+ end
end
diff --git a/spec/support/shared_examples/workers/project_export_shared_examples.rb b/spec/support/shared_examples/workers/project_export_shared_examples.rb
index a9bcc3f4f7c..175ef9bd012 100644
--- a/spec/support/shared_examples/workers/project_export_shared_examples.rb
+++ b/spec/support/shared_examples/workers/project_export_shared_examples.rb
@@ -53,6 +53,10 @@ RSpec.shared_examples 'export worker' do
it 'does not raise an exception when strategy is invalid' do
expect(::Projects::ImportExport::ExportService).not_to receive(:new)
+ expect_next_instance_of(ProjectExportJob) do |job|
+ expect(job).to receive(:finish)
+ end
+
expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.not_to raise_error
end
@@ -63,6 +67,18 @@ RSpec.shared_examples 'export worker' do
it 'does not raise error when user cannot be found' do
expect { subject.perform(non_existing_record_id, project.id, {}) }.not_to raise_error
end
+
+ it 'fails the export job status' do
+ expect_next_instance_of(::Projects::ImportExport::ExportService) do |service|
+ expect(service).to receive(:execute).and_raise(Gitlab::ImportExport::Error)
+ end
+
+ expect_next_instance_of(ProjectExportJob) do |job|
+ expect(job).to receive(:fail_op)
+ end
+
+ expect { subject.perform(user.id, project.id, {}) }.to raise_error(Gitlab::ImportExport::Error)
+ end
end
end
diff --git a/spec/workers/auto_devops/disable_worker_spec.rb b/spec/workers/auto_devops/disable_worker_spec.rb
index 239f4b09f5c..e1de97e0ce5 100644
--- a/spec/workers/auto_devops/disable_worker_spec.rb
+++ b/spec/workers/auto_devops/disable_worker_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe AutoDevops::DisableWorker, '#perform' do
let(:namespace) { create(:namespace, owner: owner) }
let(:project) { create(:project, :repository, :auto_devops, namespace: namespace) }
- it 'sends an email to pipeline user and project owner' do
+ it 'sends an email to pipeline user and project owner(s)' do
expect(NotificationService).to receive_message_chain(:new, :autodevops_disabled).with(pipeline, [user.email, owner.email])
subject.perform(pipeline.id)