diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index 4f6f0e09fd5..0748fd35267 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -35,7 +35,7 @@ review-docs-cleanup:
.docs-markdown-lint-image:
# When updating the image version here, update it in /scripts/lint-doc.sh too.
- image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/technical-writing/docs-gitlab-com/lint-markdown:alpine-3.21-vale-3.9.3-markdownlint2-0.17.1-lychee-0.18.0
+ image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/technical-writing/docs-gitlab-com/lint-markdown:alpine-3.21-vale-3.11.2-markdownlint2-0.17.2-lychee-0.18.1
docs-lint markdown:
extends:
diff --git a/.vale.ini b/.vale.ini
index 6111cc3a749..0eb17152be7 100644
--- a/.vale.ini
+++ b/.vale.ini
@@ -5,6 +5,8 @@
StylesPath = doc/.vale
MinAlertLevel = suggestion
+IgnoredScopes = code, text.frontmatter.redirect_to
+
[*.md]
BasedOnStyles = gitlab_base, gitlab_docs
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 06f16753ec9..b7ffb888a64 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -631,7 +631,7 @@
{"name":"rubocop-rspec","version":"3.0.5","platform":"ruby","checksum":"c6a8e29fb1b00d227c32df159e92f5ebb9e0ff734e52955fb13aff5c74977e0f"},
{"name":"rubocop-rspec_rails","version":"2.30.0","platform":"ruby","checksum":"888112e83f9d7ef7ad2397e9d69a0b9614a4bae24f072c399804a180f80c4c46"},
{"name":"ruby-fogbugz","version":"0.3.0","platform":"ruby","checksum":"5e04cde474648f498a71cf1e1a7ab42c66b953862fbe224f793ec0a7a1d5f657"},
-{"name":"ruby-lsp","version":"0.23.10","platform":"ruby","checksum":"71dfb08ff3bdc66f92c18e49f7ce3fe772b25804bcd08a4369f70bcad1534d6c"},
+{"name":"ruby-lsp","version":"0.23.13","platform":"ruby","checksum":"a1875a9905a79a41c63d8df52bd016f238d635b64c8f0aac3639336bcf659f48"},
{"name":"ruby-lsp-rails","version":"0.3.31","platform":"ruby","checksum":"670aed466e54b5632e4907b8dedb91d8b144917c42513e013d656af175bf8c76"},
{"name":"ruby-lsp-rspec","version":"0.1.22","platform":"ruby","checksum":"e982edf5cd6ec1530c3f5fa7e423624ad00532ebeff7fc94e02c7516a9b759c0"},
{"name":"ruby-magic","version":"0.6.0","platform":"ruby","checksum":"7b2138877b7d23aff812c95564eba6473b74b815ef85beb0eb792e729a2b6101"},
@@ -754,7 +754,7 @@
{"name":"typhoeus","version":"1.4.1","platform":"ruby","checksum":"1c17db8364bd45ab302dc61e460173c3e69835896be88a3df07c206d5c55ef7c"},
{"name":"tzinfo","version":"2.0.6","platform":"ruby","checksum":"8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b"},
{"name":"uber","version":"0.1.0","platform":"ruby","checksum":"5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc"},
-{"name":"undercover","version":"0.6.3","platform":"ruby","checksum":"a74c4246bc3ed0a506681f9cc41e2cf353c12f1544bb2b7798807e81f2cb65fa"},
+{"name":"undercover","version":"0.6.4","platform":"ruby","checksum":"3c34fcf129b52a4993065c52612a65e5e05e77f0cac3f4f8f388114fb129ec1a"},
{"name":"unf","version":"0.1.4","platform":"java","checksum":"49a5972ec0b3d091d3b0b2e00113f2f342b9b212f0db855eb30a629637f6d302"},
{"name":"unf","version":"0.1.4","platform":"ruby","checksum":"4999517a531f2a955750f8831941891f6158498ec9b6cb1c81ce89388e63022e"},
{"name":"unf_ext","version":"0.0.8.2","platform":"ruby","checksum":"90b9623ee359cc4878461c5d2eab7d3d3ce5801a680a9e7ac83b8040c5b742fa"},
diff --git a/Gemfile.lock b/Gemfile.lock
index bc55799c4cb..e8098875896 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1698,7 +1698,7 @@ GEM
ruby-fogbugz (0.3.0)
crack (~> 0.4)
multipart-post (~> 2.0)
- ruby-lsp (0.23.10)
+ ruby-lsp (0.23.13)
language_server-protocol (~> 3.17.0)
prism (>= 1.2, < 2.0)
rbs (>= 3, < 4)
@@ -1910,11 +1910,12 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
- undercover (0.6.3)
+ undercover (0.6.4)
+ base64
bigdecimal
imagen (>= 0.2.0)
rainbow (>= 2.1, < 4.0)
- rugged (>= 0.27, < 1.8)
+ rugged (>= 0.27, < 1.10)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum
index 5b5d8480963..46debf5aa35 100644
--- a/Gemfile.next.checksum
+++ b/Gemfile.next.checksum
@@ -641,7 +641,7 @@
{"name":"rubocop-rspec","version":"3.0.5","platform":"ruby","checksum":"c6a8e29fb1b00d227c32df159e92f5ebb9e0ff734e52955fb13aff5c74977e0f"},
{"name":"rubocop-rspec_rails","version":"2.30.0","platform":"ruby","checksum":"888112e83f9d7ef7ad2397e9d69a0b9614a4bae24f072c399804a180f80c4c46"},
{"name":"ruby-fogbugz","version":"0.3.0","platform":"ruby","checksum":"5e04cde474648f498a71cf1e1a7ab42c66b953862fbe224f793ec0a7a1d5f657"},
-{"name":"ruby-lsp","version":"0.23.10","platform":"ruby","checksum":"71dfb08ff3bdc66f92c18e49f7ce3fe772b25804bcd08a4369f70bcad1534d6c"},
+{"name":"ruby-lsp","version":"0.23.13","platform":"ruby","checksum":"a1875a9905a79a41c63d8df52bd016f238d635b64c8f0aac3639336bcf659f48"},
{"name":"ruby-lsp-rails","version":"0.3.31","platform":"ruby","checksum":"670aed466e54b5632e4907b8dedb91d8b144917c42513e013d656af175bf8c76"},
{"name":"ruby-lsp-rspec","version":"0.1.22","platform":"ruby","checksum":"e982edf5cd6ec1530c3f5fa7e423624ad00532ebeff7fc94e02c7516a9b759c0"},
{"name":"ruby-magic","version":"0.6.0","platform":"ruby","checksum":"7b2138877b7d23aff812c95564eba6473b74b815ef85beb0eb792e729a2b6101"},
@@ -767,7 +767,7 @@
{"name":"typhoeus","version":"1.4.1","platform":"ruby","checksum":"1c17db8364bd45ab302dc61e460173c3e69835896be88a3df07c206d5c55ef7c"},
{"name":"tzinfo","version":"2.0.6","platform":"ruby","checksum":"8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b"},
{"name":"uber","version":"0.1.0","platform":"ruby","checksum":"5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc"},
-{"name":"undercover","version":"0.6.3","platform":"ruby","checksum":"a74c4246bc3ed0a506681f9cc41e2cf353c12f1544bb2b7798807e81f2cb65fa"},
+{"name":"undercover","version":"0.6.4","platform":"ruby","checksum":"3c34fcf129b52a4993065c52612a65e5e05e77f0cac3f4f8f388114fb129ec1a"},
{"name":"unf","version":"0.1.4","platform":"java","checksum":"49a5972ec0b3d091d3b0b2e00113f2f342b9b212f0db855eb30a629637f6d302"},
{"name":"unf","version":"0.1.4","platform":"ruby","checksum":"4999517a531f2a955750f8831941891f6158498ec9b6cb1c81ce89388e63022e"},
{"name":"unf_ext","version":"0.0.8.2","platform":"ruby","checksum":"90b9623ee359cc4878461c5d2eab7d3d3ce5801a680a9e7ac83b8040c5b742fa"},
diff --git a/Gemfile.next.lock b/Gemfile.next.lock
index adbbab95ffc..aa86b16b2c8 100644
--- a/Gemfile.next.lock
+++ b/Gemfile.next.lock
@@ -1730,7 +1730,7 @@ GEM
ruby-fogbugz (0.3.0)
crack (~> 0.4)
multipart-post (~> 2.0)
- ruby-lsp (0.23.10)
+ ruby-lsp (0.23.13)
language_server-protocol (~> 3.17.0)
prism (>= 1.2, < 2.0)
rbs (>= 3, < 4)
@@ -1944,11 +1944,12 @@ GEM
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
uber (0.1.0)
- undercover (0.6.3)
+ undercover (0.6.4)
+ base64
bigdecimal
imagen (>= 0.2.0)
rainbow (>= 2.1, < 4.0)
- rugged (>= 0.27, < 1.8)
+ rugged (>= 0.27, < 1.10)
unf (0.1.4)
unf_ext
unf_ext (0.0.8.2)
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
index b04da6ddb7e..33010842e0e 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
@@ -14,6 +14,7 @@ import {
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CrudComponent from '~/vue_shared/components/crud_component.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { convertEnvironmentScope } from '~/ci/common/private/ci_environments_dropdown';
import {
DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT,
@@ -69,6 +70,7 @@ export default {
GlTable,
GlModal,
CrudComponent,
+ ClipboardButton,
},
directives: {
GlModalDirective,
@@ -273,15 +275,12 @@ export default {
class="gl-inline-block gl-max-w-full gl-break-anywhere"
>{{ item.key }}
-
@@ -304,7 +303,7 @@ export default {
v-if="!item.hidden"
class="-gl-mr-3 gl-flex gl-items-start gl-justify-end md:gl-justify-start"
>
- *****
+ •••••
{{ item.value }}
-
@@ -331,15 +327,12 @@ export default {
class="gl-inline-block gl-max-w-full gl-break-anywhere"
>{{ convertEnvironmentScopeValue(item.environmentScope) }}
-
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
index 038f406de87..3aa037fbbdc 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
@@ -309,18 +309,22 @@ export default {
>
-
+
+
+
+
+
-
+
🎉 {{ s__("Runners|You've registered a new runner!") }}
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index adacf77bfcf..ad01b34f9c9 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -150,6 +150,11 @@ export const GROUP_TYPE = 'GROUP_TYPE';
export const PROJECT_TYPE = 'PROJECT_TYPE';
export const RUNNER_TYPES = [INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE];
+// CiRunnerCreationState
+
+export const CREATION_STATE_STARTED = 'STARTED';
+export const CREATION_STATE_FINISHED = 'FINISHED';
+
// CiRunnerStatus
export const STATUS_ONLINE = 'ONLINE';
diff --git a/app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql b/app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql
index f6cee807620..62866ddddb5 100644
--- a/app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql
@@ -3,6 +3,6 @@ query getRunnerForRegistration($id: CiRunnerID!) {
id
description
ephemeralAuthenticationToken
- status
+ creationState
}
}
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 796a47bf1d9..331c662320a 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -787,6 +787,7 @@ export default {
diff --git a/app/assets/javascripts/diffs/components/diffs_file_tree.vue b/app/assets/javascripts/diffs/components/diffs_file_tree.vue
index a35cf3c0af9..1174340602c 100644
--- a/app/assets/javascripts/diffs/components/diffs_file_tree.vue
+++ b/app/assets/javascripts/diffs/components/diffs_file_tree.vue
@@ -29,6 +29,11 @@ export default {
required: false,
default: false,
},
+ totalFilesCount: {
+ type: [Number, String],
+ default: undefined,
+ required: false,
+ },
},
data() {
const treeWidth =
@@ -133,6 +138,7 @@ export default {
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 7cd03c90d07..19859b504d4 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -41,6 +41,11 @@ export default {
required: false,
default: null,
},
+ totalFilesCount: {
+ type: [Number, String],
+ default: undefined,
+ required: false,
+ },
},
data() {
return {
@@ -52,7 +57,6 @@ export default {
'renderTreeList',
'currentDiffFileId',
'viewedDiffFileIds',
- 'realSize',
'fileTree',
'allBlobs',
'linkedFile',
@@ -191,7 +195,9 @@ export default {
{{ __('Files') }}
-
{{ realSize }}
+
{{
+ totalFilesCount
+ }}
diff --git a/app/assets/javascripts/rapid_diffs/app/file_browser.vue b/app/assets/javascripts/rapid_diffs/app/file_browser.vue
index ed48994f342..e1470bf1ddc 100644
--- a/app/assets/javascripts/rapid_diffs/app/file_browser.vue
+++ b/app/assets/javascripts/rapid_diffs/app/file_browser.vue
@@ -3,6 +3,7 @@ import { mapState } from 'pinia';
import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
import { useDiffsList } from '~/rapid_diffs/stores/diffs_list';
import { useFileBrowser } from '~/diffs/stores/file_browser';
+import { useDiffsView } from '~/rapid_diffs/stores/diffs_view';
export default {
name: 'FileBrowser',
@@ -10,6 +11,7 @@ export default {
DiffsFileTree,
},
computed: {
+ ...mapState(useDiffsView, ['totalFilesCount']),
...mapState(useDiffsList, ['loadedFiles']),
...mapState(useFileBrowser, ['fileBrowserVisible']),
},
@@ -26,6 +28,7 @@ export default {
v-if="fileBrowserVisible"
floating-resize
:loaded-files="loadedFiles"
+ :total-files-count="totalFilesCount"
@clickFile="clickFile"
/>
diff --git a/app/assets/javascripts/rapid_diffs/app/index.js b/app/assets/javascripts/rapid_diffs/app/index.js
index c060c3b388d..5e7d1d12389 100644
--- a/app/assets/javascripts/rapid_diffs/app/index.js
+++ b/app/assets/javascripts/rapid_diffs/app/index.js
@@ -25,14 +25,11 @@ class RapidDiffsFacade {
document.querySelector('[data-diffs-list]'),
this.DiffFileImplementation,
);
- const { reloadStreamUrl, metadataEndpoint, diffFilesEndpoint } =
+ const { reloadStreamUrl, diffsStatsEndpoint, diffFilesEndpoint } =
document.querySelector('[data-rapid-diffs]').dataset;
- useDiffsView(pinia).metadataEndpoint = metadataEndpoint;
+ useDiffsView(pinia).diffsStatsEndpoint = diffsStatsEndpoint;
useDiffsView(pinia)
- .loadMetadata()
- .then(() => {
- initHiddenFilesWarning();
- })
+ .loadDiffsStats()
.catch((error) => {
createAlert({
message: __('Failed to load additional diffs information. Try reloading the page.'),
@@ -46,6 +43,7 @@ class RapidDiffsFacade {
});
});
initViewSettings({ pinia, streamUrl: reloadStreamUrl });
+ initHiddenFilesWarning();
document.addEventListener(DIFF_FILE_MOUNTED, useDiffsList(pinia).addLoadedFile);
}
diff --git a/app/assets/javascripts/rapid_diffs/app/init_hidden_files_warning.js b/app/assets/javascripts/rapid_diffs/app/init_hidden_files_warning.js
index 7fb995e3161..b742d962b3d 100644
--- a/app/assets/javascripts/rapid_diffs/app/init_hidden_files_warning.js
+++ b/app/assets/javascripts/rapid_diffs/app/init_hidden_files_warning.js
@@ -12,17 +12,17 @@ export async function initHiddenFilesWarning() {
el,
pinia,
computed: {
- ...mapState(useDiffsView, ['diffStats']),
+ ...mapState(useDiffsView, ['overflow', 'totalFilesCount']),
},
render(h) {
- if (!this.diffStats?.renderOverflowWarning) return null;
+ if (!this.overflow) return null;
return h(HiddenFilesWarning, {
props: {
- total: this.diffStats?.realSize,
- visible: this.diffStats?.size,
- plainDiffPath: this.diffStats?.plainDiffPath,
- emailPatchPath: this.diffStats?.emailPatchPath,
+ total: this.totalFilesCount,
+ visible: this.overflow?.visibleCount,
+ plainDiffPath: this.overflow?.diffPath,
+ emailPatchPath: this.overflow?.emailPath,
},
});
},
diff --git a/app/assets/javascripts/rapid_diffs/app/view_settings.js b/app/assets/javascripts/rapid_diffs/app/view_settings.js
index 819ad296308..0894e961aa6 100644
--- a/app/assets/javascripts/rapid_diffs/app/view_settings.js
+++ b/app/assets/javascripts/rapid_diffs/app/view_settings.js
@@ -26,7 +26,7 @@ const initSettingsApp = (el, pinia) => {
'viewType',
'fileByFileMode',
'singleFileMode',
- 'diffStats',
+ 'diffsStats',
]),
},
methods: {
@@ -40,9 +40,9 @@ const initSettingsApp = (el, pinia) => {
diffViewType: this.viewType,
viewDiffsFileByFile: this.singleFileMode,
isLoading: this.isLoading,
- addedLines: this.diffStats?.addedLines,
- removedLines: this.diffStats?.removedLines,
- diffsCount: this.diffStats?.diffsCount,
+ addedLines: this.diffsStats?.addedLines,
+ removedLines: this.diffsStats?.removedLines,
+ diffsCount: this.diffsStats?.diffsCount,
},
on: {
updateDiffViewType: this.updateViewType,
diff --git a/app/assets/javascripts/rapid_diffs/stores/diffs_view.js b/app/assets/javascripts/rapid_diffs/stores/diffs_view.js
index dccf3f875c8..647ac479f92 100644
--- a/app/assets/javascripts/rapid_diffs/stores/diffs_view.js
+++ b/app/assets/javascripts/rapid_diffs/stores/diffs_view.js
@@ -11,7 +11,6 @@ import { queueRedisHllEvents } from '~/diffs/utils/queue_events';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import { useDiffsList } from '~/rapid_diffs/stores/diffs_list';
-import store from '~/mr_notes/stores';
export const useDiffsView = defineStore('diffsView', {
state() {
@@ -21,28 +20,38 @@ export const useDiffsView = defineStore('diffsView', {
singleFileMode: false,
updateUserEndpoint: undefined,
streamUrl: undefined,
- metadataEndpoint: undefined,
- diffStats: null,
+ diffsStatsEndpoint: undefined,
+ diffsStats: null,
+ overflow: null,
};
},
actions: {
- async loadMetadata() {
- // TODO: refactor this to our own Pinia stores
- store.state.diffs.endpointMetadata = this.metadataEndpoint;
- store.state.diffs.diffViewType = this.viewType;
- store.state.diffs.showWhitespace = this.showWhitespace;
- await store.dispatch('diffs/fetchDiffFilesMeta');
- this.diffStats = {
- addedLines: store.state.diffs.addedLines,
- removedLines: store.state.diffs.removedLines,
- size: store.state.diffs.size,
- realSize: store.state.diffs.realSize,
- plainDiffPath: store.state.diffs.plainDiffPath,
- emailPatchPath: store.state.diffs.emailPatchPath,
- renderOverflowWarning: store.state.diffs.renderOverflowWarning,
- // we will be using a number for that after refactoring
- diffsCount: parseInt(store.state.diffs.realSize, 10),
+ async loadDiffsStats() {
+ const { data } = await axios.get(this.diffsStatsEndpoint);
+ const {
+ added_lines: addedLines,
+ removed_lines: removedLines,
+ diffs_count: diffsCount,
+ } = data.diffs_stats;
+ this.diffsStats = {
+ addedLines,
+ removedLines,
+ diffsCount,
};
+ if (data.overflow) {
+ const {
+ visible_count: visibleCount,
+ email_path: emailPath,
+ diff_path: diffPath,
+ } = data.overflow || {};
+ this.overflow = {
+ visibleCount,
+ emailPath,
+ diffPath,
+ };
+ } else {
+ this.overflow = null;
+ }
},
updateDiffView() {
if (this.singleFileMode) {
@@ -68,7 +77,6 @@ export const useDiffsView = defineStore('diffsView', {
// we don't have to wait for the setting to be saved since whitespace param is passed explicitly
axios.put(this.updateUserEndpoint, { show_whitespace_in_diffs: value });
}
- this.loadMetadata();
this.updateDiffView();
},
},
@@ -77,5 +85,8 @@ export const useDiffsView = defineStore('diffsView', {
// w: '1' means ignore whitespace, app/helpers/diff_helper.rb#hide_whitespace?
return { view: this.viewType, w: this.showWhitespace ? '0' : '1' };
},
+ totalFilesCount() {
+ return this.diffsStats?.diffsCount;
+ },
},
});
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_header_app.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_header_app.vue
index 505d8135634..1fd694074a1 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_header_app.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_header_app.vue
@@ -1,6 +1,6 @@
@@ -63,26 +52,33 @@ export default {
-
+
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
index 191e61d8ce7..a2c29967629 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
@@ -20,6 +20,11 @@ export default {
required: false,
default: false,
},
+ fluidWidth: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
label: {
type: String,
required: true,
@@ -207,6 +212,7 @@ export default {
ref="listbox"
v-model="selected"
:block="block"
+ :fluid-width="fluidWidth"
:header-text="headerText"
:reset-button-label="resetButtonLabel"
:toggle-text="toggleText"
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
index a72143ec4a9..53981cbc8ac 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
@@ -29,6 +29,11 @@ export default {
required: false,
default: false,
},
+ fluidWidth: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
label: {
type: String,
required: false,
@@ -141,6 +146,7 @@ export default {
:fetch-items="fetchGroups"
:fetch-initial-selection="fetchInitialGroup"
:block="block"
+ :fluid-width="fluidWidth"
v-on="$listeners"
>
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 566cb73d7eb..360e99d6d95 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -4,6 +4,13 @@ html {
}
}
+// Do not change or manually disable this
+// Instead, call `- disable_fixed_body_scroll` inside your page's HAML template if you need to hide the scroll
+// Custom class is used instead of Tailwind so people can discover this, do not replace this with Tailwind analog
+.body-fixed-scrollbar {
+ overflow-y: scroll;
+}
+
.container-fluid {
&.limit-container-width {
.flash-container.sticky {
diff --git a/app/components/rapid_diffs/app_component.html.haml b/app/components/rapid_diffs/app_component.html.haml
index cf4fa803b34..89488bc715e 100644
--- a/app/components/rapid_diffs/app_component.html.haml
+++ b/app/components/rapid_diffs/app_component.html.haml
@@ -1,5 +1,5 @@
- if !@lazy
- - helpers.add_page_startup_api_call @metadata_endpoint
+ - helpers.add_page_startup_api_call @diffs_stats_endpoint
- helpers.add_page_startup_api_call @diff_files_endpoint
- if @stream_url
- helpers.content_for :startup_js do
@@ -11,7 +11,7 @@
streamRequest: fetch('#{Gitlab::UrlSanitizer.sanitize(@stream_url)}', { signal: controller.signal })
}
-.rd-app{ data: { rapid_diffs: true, reload_stream_url: @reload_stream_url, metadata_endpoint: @metadata_endpoint, diff_files_endpoint: @diff_files_endpoint } }
+.rd-app{ data: { rapid_diffs: true, reload_stream_url: @reload_stream_url, diffs_stats_endpoint: @diffs_stats_endpoint, diff_files_endpoint: @diff_files_endpoint } }
.rd-app-header
.rd-app-file-browser-toggle
%div{ data: { file_browser_toggle: true } }
diff --git a/app/components/rapid_diffs/app_component.rb b/app/components/rapid_diffs/app_component.rb
index 039249a772d..4250f4e8afb 100644
--- a/app/components/rapid_diffs/app_component.rb
+++ b/app/components/rapid_diffs/app_component.rb
@@ -11,7 +11,7 @@ module RapidDiffs
show_whitespace:,
diff_view:,
update_user_endpoint:,
- metadata_endpoint:,
+ diffs_stats_endpoint:,
diff_files_endpoint:,
lazy: false
)
@@ -21,7 +21,7 @@ module RapidDiffs
@show_whitespace = show_whitespace
@diff_view = diff_view
@update_user_endpoint = update_user_endpoint
- @metadata_endpoint = metadata_endpoint
+ @diffs_stats_endpoint = diffs_stats_endpoint
@diff_files_endpoint = diff_files_endpoint
@lazy = lazy
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 004bc898420..b1e62ca1677 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -19,6 +19,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
before_action only: [:index] do
push_frontend_feature_flag(:importer_user_mapping, current_user)
push_frontend_feature_flag(:importer_user_mapping_reassignment_csv, current_user)
+ push_frontend_feature_flag(:importer_user_mapping_allow_bypass_of_confirmation, @group)
push_frontend_feature_flag(:service_accounts_crud, @group)
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 30e045cf7c1..7e45a3468c2 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -166,6 +166,7 @@ class Projects::CommitController < Projects::ApplicationController
@stream_url = diffs_stream_url(@commit, streaming_offset, diff_view)
@diffs_slice = @commit.first_diffs_slice(streaming_offset, commit_diff_options)
@diff_files_endpoint = diff_files_metadata_namespace_project_commit_path
+ @diffs_stats_endpoint = diffs_stats_namespace_project_commit_path
show
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 328573c5a3a..55f718cfb66 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -83,6 +83,7 @@ class Projects::CompareController < Projects::ApplicationController
@show_whitespace_default = current_user.nil? || current_user.show_whitespace_in_diffs
@reload_stream_url = diffs_stream_namespace_project_compare_index_path(**compare_params)
@diff_files_endpoint = diff_files_metadata_namespace_project_compare_index_path(**compare_params)
+ @diffs_stats_endpoint = diffs_stats_namespace_project_compare_index_path(**compare_params)
@update_current_user_path = expose_path(api_v4_user_preferences_path)
show
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 6ec02a31cc6..a930fee6eca 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -70,6 +70,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@stream_url = project_new_merge_request_diffs_stream_path(@project, merge_request: merge_request)
@reload_stream_url = project_new_merge_request_diffs_stream_path(@project, merge_request: merge_request)
@diff_files_endpoint = project_new_merge_request_diff_files_metadata_path(@project, merge_request: merge_request)
+ @diffs_stats_endpoint = project_new_merge_request_diffs_stats_path(@project, merge_request: merge_request)
define_new_vars
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index ef0a0d3a449..f78a973739c 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -113,6 +113,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@stream_url = diffs_stream_url(@merge_request, streaming_offset, diff_view)
@diffs_slice = @merge_request.first_diffs_slice(streaming_offset)
@diff_files_endpoint = diff_files_metadata_namespace_project_merge_request_path
+ @diffs_stats_endpoint = diffs_stats_namespace_project_merge_request_path
show_merge_request
end
diff --git a/app/graphql/mutations/ci/runner/bulk_pause.rb b/app/graphql/mutations/ci/runner/bulk_pause.rb
new file mode 100644
index 00000000000..3f038eb08eb
--- /dev/null
+++ b/app/graphql/mutations/ci/runner/bulk_pause.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module Runner
+ class BulkPause < BaseMutation
+ graphql_name 'RunnerBulkPause'
+
+ RunnerID = ::Types::GlobalIDType[::Ci::Runner]
+
+ argument :ids, [RunnerID],
+ required: true,
+ description: 'IDs of the runners to pause or unpause.'
+
+ argument :paused, GraphQL::Types::Boolean,
+ required: true,
+ description: 'Indicates the runner is not allowed to receive jobs.'
+
+ field :updated_count,
+ ::GraphQL::Types::Int,
+ null: true,
+ description: 'Number of records effectively updated. ' \
+ 'Only present if operation was performed synchronously.'
+
+ field :updated_runners, # rubocop:disable GraphQL/ExtractType -- Same as bulk_delete
+ [Types::Ci::RunnerType],
+ null: true,
+ description: 'Runners after mutation.'
+
+ def resolve(**runner_attrs)
+ response = { updated_count: 0, updated_runners: [], errors: [] }
+ ids = runner_attrs[:ids]
+ runner_ids = model_ids_of(ids)
+ runners = find_all_runners_by_ids(runner_ids)
+ if runners.any?
+ result = ::Ci::Runners::BulkPauseRunnersService
+ .new(runners: runners, current_user: current_user, paused: runner_attrs[:paused])
+ .execute
+ result.payload.slice(:updated_count, :updated_runners, :errors)
+ else
+ response
+ end
+ end
+
+ private
+
+ def model_ids_of(global_ids)
+ global_ids.filter_map { |gid| gid.model_id.to_i }
+ end
+
+ def find_all_runners_by_ids(ids)
+ return ::Ci::Runner.none if ids.blank?
+
+ limit = ::Ci::Runners::BulkPauseRunnersService::RUNNER_LIMIT
+ ::Ci::Runner.id_in(ids).limit(limit + 1)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_creation_state_enum.rb b/app/graphql/types/ci/runner_creation_state_enum.rb
index 3f286b54cf7..1a18bff6301 100644
--- a/app/graphql/types/ci/runner_creation_state_enum.rb
+++ b/app/graphql/types/ci/runner_creation_state_enum.rb
@@ -6,10 +6,10 @@ module Types
graphql_name 'CiRunnerCreationState'
value 'STARTED',
- description: 'Applies to a runner that has been created, but not is not yet registered and running.',
+ description: 'Applies to a runner that has been created, but is not yet registered and running.',
value: 'started'
value 'FINISHED',
- description: 'Applies to a runner that has been registered and has polled for CI jobs at least once.',
+ description: 'Applies to a runner that has been registered and has polled for CI/CD jobs at least once.',
value: 'finished'
end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index a30393bb47d..d33722237f9 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -203,6 +203,7 @@ module Types
mount_mutation Mutations::Ci::PipelineTrigger::Update, experiment: { milestone: '16.3' }
mount_mutation Mutations::Ci::ProjectCiCdSettingsUpdate
mount_mutation Mutations::Ci::Runner::BulkDelete, experiment: { milestone: '15.3' }
+ mount_mutation Mutations::Ci::Runner::BulkPause, experiment: { milestone: '17.11' }
mount_mutation Mutations::Ci::Runner::Cache::Clear
mount_mutation Mutations::Ci::Runner::Create, experiment: { milestone: '15.10' }
mount_mutation Mutations::Ci::Runner::Delete
diff --git a/app/graphql/types/work_items/widgets/award_emoji_type.rb b/app/graphql/types/work_items/widgets/award_emoji_type.rb
index cdaeb8a31d0..57985e2697b 100644
--- a/app/graphql/types/work_items/widgets/award_emoji_type.rb
+++ b/app/graphql/types/work_items/widgets/award_emoji_type.rb
@@ -20,6 +20,10 @@ module Types
GraphQL::Types::Int,
null: false,
description: 'Number of downvotes the work item has received.'
+ field :new_custom_emoji_path,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Path to create a new custom emoji.'
field :upvotes,
GraphQL::Types::Int,
null: false,
@@ -34,6 +38,12 @@ module Types
BatchLoaders::AwardEmojiVotesBatchLoader
.load_upvotes(object.work_item, awardable_class: 'Issue')
end
+
+ def new_custom_emoji_path
+ return unless context[:current_user]&.can?(:create_custom_emoji, object.work_item.project.namespace)
+
+ ::Gitlab::Routing.url_helpers.new_group_custom_emoji_path(object.work_item.project.namespace)
+ end
end
# rubocop:enable Graphql/AuthorizeTypes
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 542248002f0..9e2cd1491df 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -331,6 +331,18 @@ module ApplicationHelper
class_names
end
+ def disable_fixed_body_scroll
+ content_for :disable_fixed_body_scroll, true
+ end
+
+ def body_scroll_classes
+ return '' unless Feature.enabled?(:force_scrollbar, current_user, type: :beta)
+ return '' if content_for(:disable_fixed_body_scroll).present?
+
+ # Custom class is used instead of Tailwind so people can discover this, do not replace this with Tailwind analog
+ 'body-fixed-scrollbar'
+ end
+
def system_message_class
class_names = []
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 2ca36d05cd6..06b8cb36188 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -495,14 +495,15 @@ module Ci
ensure_runner_queue_value == value if value.present?
end
- def heartbeat
+ def heartbeat(creation_state: nil)
##
# We can safely ignore writes performed by a runner heartbeat. We do
# not want to upgrade database connection proxy to use the primary
# database after heartbeat write happens.
#
::Gitlab::Database::LoadBalancing::SessionMap.current(load_balancer).without_sticky_writes do
- values = { contacted_at: Time.current, creation_state: :finished }
+ values = { contacted_at: Time.current }
+ values[:creation_state] = creation_state if creation_state.present?
merge_cache_attributes(values)
@@ -511,13 +512,6 @@ module Ci
end
end
- def clear_heartbeat
- cleared_attributes = { contacted_at: nil }
-
- merge_cache_attributes(cleared_attributes)
- update_columns(cleared_attributes)
- end
-
def pick_build!(build)
tick_runner_queue if matches_build?(build)
end
diff --git a/app/models/ci/runner_manager.rb b/app/models/ci/runner_manager.rb
index d37adad7945..02d1dae73c9 100644
--- a/app/models/ci/runner_manager.rb
+++ b/app/models/ci/runner_manager.rb
@@ -140,7 +140,7 @@ module Ci
read_attribute(:contacted_at)
end
- def heartbeat(values, update_contacted_at: true)
+ def heartbeat(values)
##
# We can safely ignore writes performed by a runner heartbeat. We do
# not want to upgrade database connection proxy to use the primary
@@ -150,7 +150,7 @@ module Ci
values = values&.slice(:version, :revision, :platform, :architecture, :ip_address, :config,
:executor, :runtime_features) || {}
- values.merge!(contacted_at: Time.current, creation_state: :finished) if update_contacted_at
+ values.merge!(contacted_at: Time.current, creation_state: :finished)
if values.include?(:executor)
values[:executor_type] = EXECUTOR_NAME_TO_TYPES.fetch(values.delete(:executor), :unknown)
diff --git a/app/models/clusters/agents/managed_resource.rb b/app/models/clusters/agents/managed_resource.rb
index 817712866f9..1ec85f811b0 100644
--- a/app/models/clusters/agents/managed_resource.rb
+++ b/app/models/clusters/agents/managed_resource.rb
@@ -18,6 +18,12 @@ module Clusters
completed: 1,
failed: 2
}
+
+ enum :deletion_strategy, {
+ never: 0,
+ on_stop: 1,
+ on_delete: 2
+ }
end
end
end
diff --git a/app/models/concerns/ci/has_runner_status.rb b/app/models/concerns/ci/has_runner_status.rb
index e253162161d..6c24dc09473 100644
--- a/app/models/concerns/ci/has_runner_status.rb
+++ b/app/models/concerns/ci/has_runner_status.rb
@@ -32,11 +32,12 @@ module Ci
def status
return :stale if stale?
+ return :never_contacted if contacted_at.nil?
# NOTE: We can't use finished_creation_state? here as we need to check cached value
- return :never_contacted unless creation_state == 'finished'
+ return :online if online? && creation_state == 'finished'
- online? ? :online : :offline
+ :offline
end
def online?
diff --git a/app/models/concerns/use_sql_function_for_primary_key_lookups.rb b/app/models/concerns/use_sql_function_for_primary_key_lookups.rb
index 46089b6967f..0c859f26aad 100644
--- a/app/models/concerns/use_sql_function_for_primary_key_lookups.rb
+++ b/app/models/concerns/use_sql_function_for_primary_key_lookups.rb
@@ -5,8 +5,6 @@ module UseSqlFunctionForPrimaryKeyLookups
class_methods do
def _query_by_sql(sql, ...)
- return super unless Feature.enabled?(:use_sql_functions_for_primary_key_lookups, Feature.current_request)
-
replaced = try_replace_with_function_call(sql)
return super unless replaced
@@ -14,9 +12,7 @@ module UseSqlFunctionForPrimaryKeyLookups
super(replaced.arel, ...)
end
- def cached_find_by_statement(key, &block)
- return super unless Feature.enabled?(:use_sql_functions_for_primary_key_lookups, Feature.current_request)
-
+ def cached_find_by_statement(key, &_block)
transformed_block = proc do |params|
original = yield(params)
diff --git a/app/services/ci/runners/bulk_pause_runners_service.rb b/app/services/ci/runners/bulk_pause_runners_service.rb
new file mode 100644
index 00000000000..7411a88ce3d
--- /dev/null
+++ b/app/services/ci/runners/bulk_pause_runners_service.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module Ci
+ module Runners
+ class BulkPauseRunnersService
+ attr_reader :current_user, :runners
+
+ RUNNER_LIMIT = 50
+
+ # @param runners [Array] the runners to pause or unpause
+ # @param current_user [User] the user performing the operation
+ # @param paused [Boolean] action of pausing or unpausing
+ def initialize(runners:, current_user:, paused:)
+ @runners = runners
+ @current_user = current_user
+ @paused = paused
+ end
+
+ def execute
+ if runners.present?
+ # pause the runners
+ return pause_runners(@paused)
+ end
+
+ ServiceResponse.success(payload: { updated_count: 0, updated_runners: [], errors: [] })
+ end
+
+ private
+
+ def pause_runners(paused)
+ active = !paused
+ runner_count = runners.limit(RUNNER_LIMIT + 1).count
+ authorized_runners_ids, unauthorized_runners_ids = compute_authorized_runners
+ runners_to_be_updated = Ci::Runner.id_in(authorized_runners_ids)
+ runners_to_be_updated.update(active: active)
+ ServiceResponse.success(
+ payload: {
+ updated_count: runners_to_be_updated.count,
+ updated_runners: runners_to_be_updated,
+ errors: error_messages(runner_count, authorized_runners_ids, unauthorized_runners_ids)
+ })
+ end
+
+ def compute_authorized_runners
+ current_user.ci_owned_runners.load # preload the owned runners to avoid an N+1
+
+ authorized_runners, unauthorized_runners =
+ runners.limit(RUNNER_LIMIT)
+ .partition { |runner| Ability.allowed?(current_user, :update_runner, runner) }
+ [authorized_runners.map(&:id), unauthorized_runners.map(&:id)]
+ end
+
+ def error_messages(runner_count, authorized_runners_ids, unauthorized_runners_ids)
+ errors = []
+
+ if runner_count > RUNNER_LIMIT
+ errors << "Can only pause up to #{RUNNER_LIMIT} runners per call. Ignored the remaining runner(s)."
+ end
+
+ if authorized_runners_ids.empty?
+ errors << "User does not have permission to update / pause any of the runners"
+ elsif unauthorized_runners_ids.any?
+ failed_ids = unauthorized_runners_ids.map { |runner_id| "##{runner_id}" }.join(', ')
+ errors << "User does not have permission to update / pause runner(s) #{failed_ids}"
+ end
+
+ errors
+ end
+ end
+ end
+end
diff --git a/app/services/ci/runners/unregister_runner_manager_service.rb b/app/services/ci/runners/unregister_runner_manager_service.rb
index b0d838d9f7d..6f00e9be50b 100644
--- a/app/services/ci/runners/unregister_runner_manager_service.rb
+++ b/app/services/ci/runners/unregister_runner_manager_service.rb
@@ -20,8 +20,6 @@ module Ci
runner_manager = runner.runner_managers.find_by_system_xid!(system_id)
runner_manager.destroy!
- runner.clear_heartbeat if runner.runner_managers.empty?
-
ServiceResponse.success
end
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
index 4ac26496338..b9a95d18804 100644
--- a/app/views/ide/index.html.haml
+++ b/app/views/ide/index.html.haml
@@ -1,3 +1,4 @@
+- disable_fixed_body_scroll
- page_title _("IDE"), @project.full_name
- add_page_specific_style 'page_bundles/web_ide_loader'
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 94f1d9256a6..f0008ef6177 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,6 +1,6 @@
- page_classes = page_class << @html_class
- page_classes = [user_application_color_mode, user_application_theme, page_classes.flatten.compact]
-- body_classes = [user_tab_width, @body_class, client_class_list, *custom_diff_color_classes]
+- body_classes = [user_tab_width, @body_class, client_class_list, body_scroll_classes, *custom_diff_color_classes]
!!! 5
%html{ lang: I18n.locale, class: page_classes }
diff --git a/app/views/projects/commit/rapid_diffs.html.haml b/app/views/projects/commit/rapid_diffs.html.haml
index f0e62bff58a..76b0206c463 100644
--- a/app/views/projects/commit/rapid_diffs.html.haml
+++ b/app/views/projects/commit/rapid_diffs.html.haml
@@ -13,5 +13,5 @@
.container-fluid{ class: [container_class] }
= render "commit_box"
= render "ci_menu"
- - args = { diffs_slice: @diffs_slice, reload_stream_url: @reload_stream_url, stream_url: @stream_url, show_whitespace: @show_whitespace_default, diff_view: @diff_view, update_user_endpoint: @update_current_user_path, metadata_endpoint: @endpoint_metadata_url, diff_files_endpoint: @diff_files_endpoint }
+ - args = { diffs_slice: @diffs_slice, reload_stream_url: @reload_stream_url, stream_url: @stream_url, show_whitespace: @show_whitespace_default, diff_view: @diff_view, update_user_endpoint: @update_current_user_path, diffs_stats_endpoint: @diffs_stats_endpoint, diff_files_endpoint: @diff_files_endpoint }
= render ::RapidDiffs::AppComponent.new(**args)
diff --git a/app/views/projects/compare/rapid_diffs.html.haml b/app/views/projects/compare/rapid_diffs.html.haml
index abeabfe53ec..f9558da164e 100644
--- a/app/views/projects/compare/rapid_diffs.html.haml
+++ b/app/views/projects/compare/rapid_diffs.html.haml
@@ -17,7 +17,7 @@
.container-fluid{ class: [container_class] }
= render "projects/commits/commit_list" unless hide_commit_list
.container-fluid
- - args = { diffs_slice: nil, reload_stream_url: @reload_stream_url, stream_url: nil, show_whitespace: @show_whitespace_default, diff_view: diff_view, metadata_endpoint: nil, update_user_endpoint: @update_current_user_path, diff_files_endpoint: @diff_files_endpoint, lazy: true }
+ - args = { diffs_slice: nil, reload_stream_url: @reload_stream_url, stream_url: nil, show_whitespace: @show_whitespace_default, diff_view: diff_view, diffs_stats_endpoint: @diffs_stats_endpoint, update_user_endpoint: @update_current_user_path, diff_files_endpoint: @diff_files_endpoint, lazy: true }
= render ::RapidDiffs::AppComponent.new(**args)
- else
.container-fluid
diff --git a/app/views/projects/merge_requests/creations/rapid_diffs.html.haml b/app/views/projects/merge_requests/creations/rapid_diffs.html.haml
index c00e30a92dd..bfe466641f8 100644
--- a/app/views/projects/merge_requests/creations/rapid_diffs.html.haml
+++ b/app/views/projects/merge_requests/creations/rapid_diffs.html.haml
@@ -3,5 +3,5 @@
- add_page_specific_style 'page_bundles/merge_request_creation_rapid_diffs'
= render "page" do
- - args = { diffs_slice: nil, reload_stream_url: @reload_stream_url, stream_url: @stream_url, show_whitespace: @show_whitespace_default, diff_view: diff_view, update_user_endpoint: expose_path(api_v4_user_preferences_path), metadata_endpoint: nil, diff_files_endpoint: @diff_files_endpoint, lazy: true }
+ - args = { diffs_slice: nil, reload_stream_url: @reload_stream_url, stream_url: @stream_url, show_whitespace: @show_whitespace_default, diff_view: diff_view, update_user_endpoint: expose_path(api_v4_user_preferences_path), diffs_stats_endpoint: @diffs_stats_endpoint, diff_files_endpoint: @diff_files_endpoint, lazy: true }
= render ::RapidDiffs::AppComponent.new(**args)
diff --git a/app/views/projects/merge_requests/rapid_diffs.html.haml b/app/views/projects/merge_requests/rapid_diffs.html.haml
index a1e5c94d890..46a53c7d7bd 100644
--- a/app/views/projects/merge_requests/rapid_diffs.html.haml
+++ b/app/views/projects/merge_requests/rapid_diffs.html.haml
@@ -3,7 +3,7 @@
- @content_class = 'rd-page-container diffs-container-limited'
= render 'page'
-- args = { diffs_slice: @diffs_slice, reload_stream_url: @reload_stream_url, stream_url: @stream_url, show_whitespace: @show_whitespace_default, diff_view: @diff_view, update_user_endpoint: @update_current_user_path, metadata_endpoint: @endpoint_metadata_url, diff_files_endpoint: @diff_files_endpoint }
+- args = { diffs_slice: @diffs_slice, reload_stream_url: @reload_stream_url, stream_url: @stream_url, show_whitespace: @show_whitespace_default, diff_view: @diff_view, update_user_endpoint: @update_current_user_path, diffs_stats_endpoint: @diffs_stats_endpoint, diff_files_endpoint: @diff_files_endpoint }
= render ::RapidDiffs::AppComponent.new(**args) do |c|
- c.with_diffs_list do
= render RapidDiffs::MergeRequestDiffFileComponent.with_collection(@diffs_slice, merge_request: @merge_request, parallel_view: @diff_view == :parallel)
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 7ea2062eea1..58aa49f1dfd 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -1,3 +1,4 @@
+- disable_fixed_body_scroll
- board = local_assigns.fetch(:board, nil)
- @no_container = true
- @content_wrapper_class = "#{@content_wrapper_class} gl-relative gl-pb-0"
diff --git a/config/feature_flags/development/use_sql_functions_for_primary_key_lookups.yml b/config/feature_flags/beta/force_scrollbar.yml
similarity index 52%
rename from config/feature_flags/development/use_sql_functions_for_primary_key_lookups.yml
rename to config/feature_flags/beta/force_scrollbar.yml
index 68fc9381348..3dfeb07efad 100644
--- a/config/feature_flags/development/use_sql_functions_for_primary_key_lookups.yml
+++ b/config/feature_flags/beta/force_scrollbar.yml
@@ -1,8 +1,9 @@
---
-name: use_sql_functions_for_primary_key_lookups
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135196
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429479
-milestone: '16.6'
-type: development
-group: group::database frameworks
+name: force_scrollbar
+feature_issue_url:
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186498
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/534572
+milestone: '17.11'
+group: group::personal productivity
+type: beta
default_enabled: false
diff --git a/config/feature_flags/ops/search_sidekiq_default_concurrency_limit.yml b/config/feature_flags/ops/search_sidekiq_default_concurrency_limit.yml
deleted file mode 100644
index 1234abd22b3..00000000000
--- a/config/feature_flags/ops/search_sidekiq_default_concurrency_limit.yml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: search_sidekiq_default_concurrency_limit
-feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/498212
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169034
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/498992
-milestone: '17.6'
-group: group::global search
-type: ops
-default_enabled: true
diff --git a/config/feature_flags/wip/importer_user_mapping_allow_bypass_of_confirmation.yml b/config/feature_flags/wip/importer_user_mapping_allow_bypass_of_confirmation.yml
new file mode 100644
index 00000000000..fa876f1a4c6
--- /dev/null
+++ b/config/feature_flags/wip/importer_user_mapping_allow_bypass_of_confirmation.yml
@@ -0,0 +1,9 @@
+---
+name: importer_user_mapping_allow_bypass_of_confirmation
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/534328
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187616
+rollout_issue_url:
+milestone: '17.11'
+group: group::import and integrate
+type: wip
+default_enabled: false
diff --git a/config/metrics/counts_all/20241014113429_orphaned_namespaces.yml b/config/metrics/counts_all/20241014113429_orphaned_namespaces.yml
index 8459fe6f778..e0cc550a3dc 100644
--- a/config/metrics/counts_all/20241014113429_orphaned_namespaces.yml
+++ b/config/metrics/counts_all/20241014113429_orphaned_namespaces.yml
@@ -2,7 +2,7 @@
key_path: counts.orphaned_namespaces
description: Whether orphaned namespaces are present
product_group: organizations
-value_type: number
+value_type: boolean
status: active
milestone: "17.6"
instrumentation_class: OrphanedNamespacesMetric
diff --git a/db/docs/incident_management_issuable_escalation_statuses.yml b/db/docs/incident_management_issuable_escalation_statuses.yml
index 5e0160d40cd..1f07fa4d383 100644
--- a/db/docs/incident_management_issuable_escalation_statuses.yml
+++ b/db/docs/incident_management_issuable_escalation_statuses.yml
@@ -8,14 +8,6 @@ description: Persists escalation status information for incidents
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65206
milestone: '14.2'
gitlab_schema: gitlab_main_cell
-desired_sharding_key:
- namespace_id:
- references: namespaces
- backfill_via:
- parent:
- foreign_key: issue_id
- table: issues
- sharding_key: namespace_id
- belongs_to: issue
+sharding_key:
+ namespace_id: namespaces
table_size: small
-desired_sharding_key_migration_job_name: BackfillIncidentManagementIssuableEscalationStatusesNamespaceId
diff --git a/db/migrate/20250403200805_add_deletion_strategy_to_clusters_managed_resources.rb b/db/migrate/20250403200805_add_deletion_strategy_to_clusters_managed_resources.rb
new file mode 100644
index 00000000000..890599d8b68
--- /dev/null
+++ b/db/migrate/20250403200805_add_deletion_strategy_to_clusters_managed_resources.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddDeletionStrategyToClustersManagedResources < Gitlab::Database::Migration[2.2]
+ milestone '17.11'
+
+ def change
+ add_column :clusters_managed_resources, :deletion_strategy, :integer, limit: 2, null: false, default: 0
+ end
+end
diff --git a/db/post_migrate/20250410024159_add_incident_management_issuable_escalation_statuses_namespace_id_not_null.rb b/db/post_migrate/20250410024159_add_incident_management_issuable_escalation_statuses_namespace_id_not_null.rb
new file mode 100644
index 00000000000..f8220edd26d
--- /dev/null
+++ b/db/post_migrate/20250410024159_add_incident_management_issuable_escalation_statuses_namespace_id_not_null.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddIncidentManagementIssuableEscalationStatusesNamespaceIdNotNull < Gitlab::Database::Migration[2.2]
+ milestone '17.11'
+ disable_ddl_transaction!
+
+ def up
+ add_not_null_constraint :incident_management_issuable_escalation_statuses, :namespace_id
+ end
+
+ def down
+ remove_not_null_constraint :incident_management_issuable_escalation_statuses, :namespace_id
+ end
+end
diff --git a/db/schema_migrations/20250403200805 b/db/schema_migrations/20250403200805
new file mode 100644
index 00000000000..88ef887e660
--- /dev/null
+++ b/db/schema_migrations/20250403200805
@@ -0,0 +1 @@
+cc4d89c14aca832615742df8bf39df63b9b577f7d18a06bdc3db077c87184ba2
\ No newline at end of file
diff --git a/db/schema_migrations/20250410024159 b/db/schema_migrations/20250410024159
new file mode 100644
index 00000000000..35a6d33f50e
--- /dev/null
+++ b/db/schema_migrations/20250410024159
@@ -0,0 +1 @@
+3bd2e48758b0c19d847b7ca71be04196a527d7f206e45863349c61dfa00cca08
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 476defa119a..64b289a585d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12602,6 +12602,7 @@ CREATE TABLE clusters_managed_resources (
status smallint DEFAULT 0 NOT NULL,
template_name text,
tracked_objects jsonb DEFAULT '[]'::jsonb NOT NULL,
+ deletion_strategy smallint DEFAULT 0 NOT NULL,
CONSTRAINT check_4f81a98847 CHECK ((char_length(template_name) <= 1024))
);
@@ -15663,7 +15664,8 @@ CREATE TABLE incident_management_issuable_escalation_statuses (
escalations_started_at timestamp with time zone,
resolved_at timestamp with time zone,
status smallint DEFAULT 0 NOT NULL,
- namespace_id bigint
+ namespace_id bigint,
+ CONSTRAINT check_ad48232311 CHECK ((namespace_id IS NOT NULL))
);
CREATE SEQUENCE incident_management_issuable_escalation_statuses_id_seq
diff --git a/doc/.vale/gitlab_base/HeadingContent.yml b/doc/.vale/gitlab_base/HeadingContent.yml
index 38b574c2f64..de9d4aa2a7b 100644
--- a/doc/.vale/gitlab_base/HeadingContent.yml
+++ b/doc/.vale/gitlab_base/HeadingContent.yml
@@ -11,10 +11,10 @@ level: warning
link: https://docs.gitlab.com/development/documentation/topic_types/concept/#concept-topic-titles
ignorecase: true
nonword: true
-scope: raw
+scope: heading
tokens:
- - '\#+ How it works'
- - '\#+ Limitations'
- - '\#+ Overview'
- - '\#+ Use cases?'
- - '\#+ Important notes?'
+ - 'How it works'
+ - 'Limitations'
+ - 'Overview'
+ - 'Use cases?'
+ - 'Important notes?'
diff --git a/doc/.vale/gitlab_base/MergeConflictMarkers.yml b/doc/.vale/gitlab_base/MergeConflictMarkers.yml
index 990cb7e9e5a..38d1d16c034 100644
--- a/doc/.vale/gitlab_base/MergeConflictMarkers.yml
+++ b/doc/.vale/gitlab_base/MergeConflictMarkers.yml
@@ -11,4 +11,4 @@ vocab: false
level: error
scope: raw
raw:
- - '\n<<<<<<< .+\n|\n=======\n|\n>>>>>>> .+\n'
+ - '\n(?:<<<<<<< .+|=======|>>>>>>> .+)\n'
diff --git a/doc/.vale/gitlab_base/NonStandardSpaces.yml b/doc/.vale/gitlab_base/NonStandardSpaces.yml
index 0b4ed0960ca..64a0efcdeaf 100644
--- a/doc/.vale/gitlab_base/NonStandardSpaces.yml
+++ b/doc/.vale/gitlab_base/NonStandardSpaces.yml
@@ -16,4 +16,4 @@ ignorecase: true
link: https://docs.gitlab.com/development/documentation/styleguide/#punctuation
scope: raw
raw:
- - '[ ]'
+ - '[\u202F\u00A0\u200B]'
diff --git a/doc/.vale/gitlab_base/OutdatedVersions.yml b/doc/.vale/gitlab_base/OutdatedVersions.yml
index 8a38c4064df..4aff11acdb8 100644
--- a/doc/.vale/gitlab_base/OutdatedVersions.yml
+++ b/doc/.vale/gitlab_base/OutdatedVersions.yml
@@ -12,4 +12,4 @@ level: suggestion
nonword: true
ignorecase: true
tokens:
- - "GitLab v?(2[^[0-9]]|4|5|6|7|8|9|10|11|12|13|14)"
+ - "GitLab v?(2[^0-9]|[4-9]|1[0-4])"
diff --git a/doc/.vale/gitlab_docs/ImagesOld.yml b/doc/.vale/gitlab_docs/ImagesOld.yml
index 1e651c10ae2..b6d5112ae5f 100644
--- a/doc/.vale/gitlab_docs/ImagesOld.yml
+++ b/doc/.vale/gitlab_docs/ImagesOld.yml
@@ -11,4 +11,4 @@ vocab: false
level: suggestion
scope: raw
raw:
- - '!\[[^\]]*\]\([^\)]*_v(1[01234]|[345789])[^\)]*\)'
+ - '!\[[^\]]*\]\([^\)]*_v(1[0-4]|[3-9])[^\)]*\)'
diff --git a/doc/administration/backup_restore/restore_gitlab.md b/doc/administration/backup_restore/restore_gitlab.md
index af64a8fe328..f7ff8a6cd73 100644
--- a/doc/administration/backup_restore/restore_gitlab.md
+++ b/doc/administration/backup_restore/restore_gitlab.md
@@ -506,7 +506,7 @@ When a server-side backup is collected, the restore process defaults to use the
node that hosts each repository is responsible for pulling the necessary backup data directly from object storage.
1. [Configure a server-side backup destination in Gitaly](../gitaly/configure_gitaly.md#configure-server-side-backups).
-1. Start a server-side backup restore process and specifying the ID of the backup you wish to restore:
+1. Start a server-side backup restore process and specifying the [ID of the backup](backup_archive_process.md#backup-id) you wish to restore:
{{< tabs >}}
@@ -529,7 +529,7 @@ sudo -u git -H bundle exec rake gitlab:backup:restore BACKUP=11493107454_2018_04
{{< tab title="Helm chart (Kubernetes)" >}}
```shell
-kubectl exec -it -- backup-utility --restore BACKUP=11493107454_2018_04_25_10.6.4-ce --repositories-server-side
+kubectl exec -it -- backup-utility --restore -t --repositories-server-side
```
When using [cron-based backups](https://docs.gitlab.com/charts/backup-restore/backup.html#cron-based-backup),
diff --git a/doc/administration/geo/replication/multiple_servers.md b/doc/administration/geo/replication/multiple_servers.md
index b3945660096..5120a7168cd 100644
--- a/doc/administration/geo/replication/multiple_servers.md
+++ b/doc/administration/geo/replication/multiple_servers.md
@@ -206,7 +206,7 @@ Follow the [Geo database replication instructions](../setup/database.md).
If using an external PostgreSQL instance, refer also to
[Geo with external PostgreSQL instances](../setup/external_database.md).
-After streaming replication is enabled in the secondary Geo site's read-replica database, then commands such as `gitlab-rake db:migrate:status:geo` will fail, until [configuration of the secondary site is complete](#step-7-copy-secrets-and-add-the-secondary-site-in-the-application), specifically [Geo configuration - Step 3. Add the secondary site](configuration.md#step-3-add-the-secondary-site).
+After enabling streaming replication, `gitlab-rake db:migrate:status:geo` fails until [configuration of the secondary site is complete](#step-7-copy-secrets-and-add-the-secondary-site-in-the-application), specifically [Geo configuration - Step 3. Add the secondary site](configuration.md#step-3-add-the-secondary-site).
### Step 4: Configure the frontend application nodes on the Geo **secondary** site
@@ -290,18 +290,18 @@ then make the following modifications:
```
{{< alert type="note" >}}
-
+`postgresql['sql_user_password'] = 'md5 digest of secret'`
If you had set up PostgreSQL cluster using the Linux package and had set
`postgresql['sql_user_password'] = 'md5 digest of secret'`, keep in
mind that `gitlab_rails['db_password']` and `geo_secondary['db_password']`
-contains the plaintext passwords. This is used to let the Rails
+contains the plaintext passwords. These configurations are used to let the Rails
nodes connect to the databases.
{{< /alert >}}
{{< alert type="note" >}}
-Make sure that current node's IP is listed in
+ Ensure that the current node's IP is listed in
`postgresql['md5_auth_cidr_addresses']` setting of the read-replica database to
allow Rails on this node to connect to PostgreSQL.
{{< /alert >}}
diff --git a/doc/administration/reference_architectures/2k_users.md b/doc/administration/reference_architectures/2k_users.md
index 86a0e4dae99..359b9a85f7c 100644
--- a/doc/administration/reference_architectures/2k_users.md
+++ b/doc/administration/reference_architectures/2k_users.md
@@ -945,7 +945,7 @@ On each node perform the following:
When you specify `https` in the `external_url`, as in the previous example,
GitLab expects that the SSL certificates are in `/etc/gitlab/ssl/`. If the
-certificates aren't present, NGINX will fail to start. For more information, see
+certificates aren't present, NGINX won't start. For more information, see
the [HTTPS documentation](https://docs.gitlab.com/omnibus/settings/ssl/).
### GitLab Rails post-configuration
diff --git a/doc/api/admin/token.md b/doc/api/admin/token.md
index d6a17107d97..360ffe6a665 100644
--- a/doc/api/admin/token.md
+++ b/doc/api/admin/token.md
@@ -18,7 +18,7 @@ Use this API to retrieve details about arbitrary tokens and to revoke them. Unli
## Token prefixes
-When making a request, `personal`, `project` or `group access` tokens must begin with `glpat` or the current [custom prefix](../../administration/settings/account_and_limit_settings.md#personal-access-token-prefix). If the token begins with a previous custom prefix, the operation will fail. Interest in support for previous custom prefixes is tracked in [issue 165663](https://gitlab.com/gitlab-org/gitlab/-/issues/165663).
+When making a request, `personal`, `project` or `group access` tokens must begin with `glpat` or the current [custom prefix](../../administration/settings/account_and_limit_settings.md#personal-access-token-prefix). If the token begins with a previous custom prefix, the operation fails. Interest in support for previous custom prefixes is tracked in [issue 165663](https://gitlab.com/gitlab-org/gitlab/-/issues/165663).
Prerequisites:
diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index cc26b441e6d..64160a42826 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -10026,6 +10026,32 @@ Input type: `RestorePagesDeploymentInput`
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| `pagesDeployment` | [`PagesDeployment!`](#pagesdeployment) | Restored Pages Deployment. |
+### `Mutation.runnerBulkPause`
+
+{{< details >}}
+**Introduced** in GitLab 17.11.
+**Status**: Experiment.
+{{< /details >}}
+
+Input type: `RunnerBulkPauseInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `ids` | [`[CiRunnerID!]!`](#cirunnerid) | IDs of the runners to pause or unpause. |
+| `paused` | [`Boolean!`](#boolean) | Indicates the runner is not allowed to receive jobs. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| `updatedCount` | [`Int`](#int) | Number of records effectively updated. Only present if operation was performed synchronously. |
+| `updatedRunners` | [`[CiRunner!]`](#cirunner) | Runners after mutation. |
+
### `Mutation.runnerCacheClear`
Input type: `RunnerCacheClearInput`
@@ -40910,6 +40936,7 @@ Represents the emoji reactions widget.
| ---- | ---- | ----------- |
| `awardEmoji` | [`AwardEmojiConnection`](#awardemojiconnection) | Emoji reactions on the work item. (see [Connections](#connections)) |
| `downvotes` | [`Int!`](#int) | Number of downvotes the work item has received. |
+| `newCustomEmojiPath` | [`String`](#string) | Path to create a new custom emoji. |
| `type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
| `upvotes` | [`Int!`](#int) | Number of upvotes the work item has received. |
@@ -42230,8 +42257,8 @@ Runner cloud provider.
| Value | Description |
| ----- | ----------- |
-| `FINISHED` | Applies to a runner that has been registered and has polled for CI jobs at least once. |
-| `STARTED` | Applies to a runner that has been created, but not is not yet registered and running. |
+| `FINISHED` | Applies to a runner that has been registered and has polled for CI/CD jobs at least once. |
+| `STARTED` | Applies to a runner that has been created, but is not yet registered and running. |
### `CiRunnerJobExecutionStatus`
diff --git a/doc/development/application_secrets.md b/doc/development/application_secrets.md
index 7f8033d96ca..f3dde0bfa4e 100644
--- a/doc/development/application_secrets.md
+++ b/doc/development/application_secrets.md
@@ -90,9 +90,9 @@ the database may be ready.
#### Add support to Omnibus GitLab and the Cloud Native GitLab charts
-Before you add a new secret to
+Before adding a new secret to
[`config/initializers/01_secret_token.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/initializers/01_secret_token.rb),
-make sure you also update Omnibus GitLab and the Cloud Native GitLab charts, or the update will fail.
+ensure you also update the GitLab Linux package and the Cloud Native GitLab charts, or the update will fail.
Both installation methods are responsible for writing the `config/secrets.yml` file.
If if they don't know about a secret, Rails attempts to write to the file, and fails because it doesn't
have write access.
@@ -100,7 +100,7 @@ have write access.
**Examples**
- [Change for self-compiled installation](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/175154)
-- [Change for Omnibus GitLab installation](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/8026)
+- [Change for Linux package installation](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/8026)
- [Change for Cloud Native installation](https://gitlab.com/gitlab-org/charts/gitlab/-/merge_requests/3988)
#### Populate the secrets in live environments
diff --git a/doc/development/cicd/cicd_reference_documentation_guide.md b/doc/development/cicd/cicd_reference_documentation_guide.md
index 5710bb61d36..09264240e11 100644
--- a/doc/development/cicd/cicd_reference_documentation_guide.md
+++ b/doc/development/cicd/cicd_reference_documentation_guide.md
@@ -2,7 +2,7 @@
stage: Verify
group: Pipeline Authoring
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
-title: Documenting the `.gitlab-ci.yml` keywords
+title: Documenting pipeline configuration keywords
---
The [CI/CD YAML syntax reference](../../ci/yaml/_index.md) uses a standard style to make it easier to use and update.
diff --git a/doc/development/distribution/_index.md b/doc/development/distribution/_index.md
index f1f81b8adad..64216b84ccf 100644
--- a/doc/development/distribution/_index.md
+++ b/doc/development/distribution/_index.md
@@ -2,7 +2,7 @@
stage: Systems
group: Distribution
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
-description: GitLab's development guidelines for Distribution
+description: Development guidelines for Distribution
title: Contribute to GitLab Distribution
---
diff --git a/doc/development/integrations/_index.md b/doc/development/integrations/_index.md
index 735fefd1e05..64719c24161 100644
--- a/doc/development/integrations/_index.md
+++ b/doc/development/integrations/_index.md
@@ -2,7 +2,7 @@
stage: Foundations
group: Import and Integrate
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
-description: GitLab's development guidelines for Integrations
+description: Development guidelines for Integrations
title: Integration development guidelines
---
@@ -185,6 +185,27 @@ attribute :wiki_page_events, default: false
If an event attribute for an existing integration changes to `true`,
this requires a data migration to back-fill the attribute value for old records.
+### Define metrics
+
+Every new integration should have five [metrics](../internal_analytics/metrics/_index.md):
+
+- Count of active projects with the given integration
+- Count of active projects inheriting the given integration
+- Count of active groups with the given integration
+- Count of active groups inheriting the given integration
+- Count of active instance-level integrations for the given integration
+
+Metrics require the model class of the integration to work. You can add metrics only together with or after the model.
+
+To create metric definitions:
+
+1. Copy the metrics created for an existing active integration.
+1. Replace all occurrences of the previous integration's name with the new integration's name.
+1. Replace `milestone` with the current milestone and `introduced_by_url` with the merge request link.
+1. Verify all other attributes have correct values by checking the [metrics guide](../internal_analytics/metrics/metrics_dictionary.md#metrics-definition-and-validation).
+
+For example, to create metric definitions for the Slack integration, you copy the metrics [1](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180122_projects_slack_active.yml), [2](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180124_groups_slack_active.yml), [3](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180127_instances_slack_active.yml), [4](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180127_instances_slack_active.yml), and [5](https://gitlab.com/gitlab-org/gitlab/blob/master/config/metrics/counts_all/20210216180129_projects_inheriting_slack_active.yml)), then replace `Slack` with the name of the new integration.
+
### Security requirements
#### All HTTP calls must use `Gitlab::HTTP`
diff --git a/doc/development/pages/_index.md b/doc/development/pages/_index.md
index b142b3cf4a5..edf41b1b529 100644
--- a/doc/development/pages/_index.md
+++ b/doc/development/pages/_index.md
@@ -2,7 +2,7 @@
stage: Plan
group: Knowledge
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
-description: GitLab's development guidelines for GitLab Pages
+description: Development guidelines for GitLab Pages
title: Contribute to GitLab Pages development
---
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index d4aad188df6..205b73e27df 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -42,7 +42,7 @@ See also the issue for [support running Jest tests in browsers](https://gitlab.c
### Debugging Jest tests
-Running `yarn jest-debug` runs Jest in debug mode, allowing you to debug/inspect as described in the [Jest docs](https://jestjs.io/docs/troubleshooting#tests-are-failing-and-you-don-t-know-why).
+Running `yarn jest-debug` runs Jest in debug mode, allowing you to debug/inspect as described in the [Jest documentation](https://jestjs.io/docs/troubleshooting#tests-are-failing-and-you-don-t-know-why).
### Timeout error
diff --git a/doc/development/webhooks.md b/doc/development/webhooks.md
index db995bf5a4e..0df632a5fad 100644
--- a/doc/development/webhooks.md
+++ b/doc/development/webhooks.md
@@ -2,7 +2,7 @@
stage: Foundations
group: Import and Integrate
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
-description: GitLab's development guidelines for webhooks
+description: Development guidelines for webhooks
title: Webhooks developer guide
---
diff --git a/doc/development/wikis.md b/doc/development/wikis.md
index 1c86c679291..e1e7f48b522 100644
--- a/doc/development/wikis.md
+++ b/doc/development/wikis.md
@@ -2,7 +2,7 @@
stage: Plan
group: Knowledge
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
-description: GitLab's development guidelines for Wikis
+description: Development guidelines for Wikis
title: Wikis development guidelines
---
diff --git a/doc/user/duo_amazon_q/setup.md b/doc/user/duo_amazon_q/setup.md
index 602ba03b8f3..f7cba25c1ba 100644
--- a/doc/user/duo_amazon_q/setup.md
+++ b/doc/user/duo_amazon_q/setup.md
@@ -158,7 +158,7 @@ Now edit the role and add the policy:
"Effect": "Allow",
"Action": [
"q:CreateOAuthAppConnection",
- "q:DeleteOAuthAppConnection",
+ "q:DeleteOAuthAppConnection"
],
"Resource": "*"
},
diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb
index 4a441b65fdd..55e4c0a2f94 100644
--- a/lib/api/ci/helpers/runner.rb
+++ b/lib/api/ci/helpers/runner.rb
@@ -12,15 +12,15 @@ module API
JOB_TOKEN_PARAM = :token
LEGACY_SYSTEM_XID = ''
- def authenticate_runner!(ensure_runner_manager: true, update_contacted_at: true)
+ def authenticate_runner!(ensure_runner_manager: true, creation_state: nil)
track_runner_authentication
forbidden! unless current_runner
- current_runner.heartbeat if update_contacted_at
+ current_runner.heartbeat(creation_state: creation_state) if ensure_runner_manager
return unless ensure_runner_manager
runner_details = get_runner_details_from_request
- current_runner_manager&.heartbeat(runner_details, update_contacted_at: update_contacted_at)
+ current_runner_manager&.heartbeat(runner_details)
end
def get_runner_details_from_request
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
index c4fef55061f..5a5b40ff647 100644
--- a/lib/api/ci/runner.rb
+++ b/lib/api/ci/runner.rb
@@ -80,7 +80,7 @@ module API
requires :token, type: String, desc: "The runner's authentication token"
end
delete '/', urgency: :low, feature_category: :runner do
- authenticate_runner!(ensure_runner_manager: false, update_contacted_at: false)
+ authenticate_runner!(ensure_runner_manager: false)
destroy_conditionally!(current_runner) do
::Ci::Runners::UnregisterRunnerService.new(current_runner, params[:token]).execute
@@ -120,11 +120,7 @@ module API
optional :system_id, type: String, desc: "The runner's system identifier"
end
post '/verify', urgency: :low, feature_category: :runner do
- # For runners that were created in the UI, we want to update the contacted_at value
- # only when it starts polling for jobs
- registering_created_runner = params[:token].start_with?(::Ci::Runner::CREATED_RUNNER_TOKEN_PREFIX)
-
- authenticate_runner!(update_contacted_at: !registering_created_runner)
+ authenticate_runner!
status 200
present current_runner, with: Entities::Ci::RunnerRegistrationDetails
@@ -194,7 +190,7 @@ module API
parser :build_json, ::Grape::Parser::Json
post '/request', urgency: :low, feature_category: :continuous_integration do
- authenticate_runner!
+ authenticate_runner!(creation_state: :finished)
unless current_runner.active?
header 'X-GitLab-Last-Update', current_runner.ensure_runner_queue_value
diff --git a/lib/gitlab/ci/build/prerequisite/managed_resource.rb b/lib/gitlab/ci/build/prerequisite/managed_resource.rb
index e09a69a825e..e871020345e 100644
--- a/lib/gitlab/ci/build/prerequisite/managed_resource.rb
+++ b/lib/gitlab/ci/build/prerequisite/managed_resource.rb
@@ -26,7 +26,17 @@ module Gitlab
managed_resource.update!(status: :failed)
raise ManagedResourceError, format_error_message(response.errors)
else
- managed_resource.update!(status: :completed, tracked_objects: response.objects.map(&:to_h))
+ managed_resource.assign_attributes(
+ status: :completed,
+ template_name: get_template.name,
+ tracked_objects: response.objects.map(&:to_h)
+ )
+
+ deletion_strategy = template_yaml['delete_resources']
+ managed_resource.deletion_strategy = deletion_strategy if deletion_strategy.present?
+
+ managed_resource.save!
+
Gitlab::InternalEvents.track_event(
'ensure_environment_for_managed_resource',
user: build.user,
@@ -52,14 +62,8 @@ module Gitlab
end
def ensure_environment
- template = begin
- get_custom_environment_template
- rescue GRPC::NotFound
- kas_client.get_default_environment_template
- end
-
rendered_template = kas_client.render_environment_template(
- template: template,
+ template: get_template,
environment: environment,
build: build)
@@ -69,6 +73,18 @@ module Gitlab
build: build)
end
+ def get_template
+ get_custom_environment_template
+ rescue GRPC::NotFound
+ kas_client.get_default_environment_template
+ end
+ strong_memoize_attr :get_template
+
+ def template_yaml
+ YAML.safe_load(get_template.data)
+ end
+ strong_memoize_attr :template_yaml
+
def get_custom_environment_template
kas_client.get_environment_template(agent: environment.cluster_agent, template_name: DEFAULT_TEMPLATE_NAME)
end
diff --git a/lib/gitlab/omniauth_initializer.rb b/lib/gitlab/omniauth_initializer.rb
index e567f8fdedb..f4aad226207 100644
--- a/lib/gitlab/omniauth_initializer.rb
+++ b/lib/gitlab/omniauth_initializer.rb
@@ -30,7 +30,10 @@ module Gitlab
authorize_params: { gl_auth_type: 'login' }
}
when ->(provider_name) { AuthHelper.saml_providers.include?(provider_name.to_sym) }
- { attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements }
+ {
+ assertion_consumer_service_url: saml_acs_url(provider_name),
+ attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
+ }
else
{}
end
@@ -39,6 +42,10 @@ module Gitlab
def full_host
proc { |_env| Settings.gitlab['base_url'] }
end
+
+ def saml_acs_url(provider_name)
+ "#{full_host}/users/auth/#{provider_name}/callback"
+ end
end
private
diff --git a/lib/gitlab/usage/metrics/instrumentations/base_integrations_metric.rb b/lib/gitlab/usage/metrics/instrumentations/base_integrations_metric.rb
index c12752cbdf7..d453549df10 100644
--- a/lib/gitlab/usage/metrics/instrumentations/base_integrations_metric.rb
+++ b/lib/gitlab/usage/metrics/instrumentations/base_integrations_metric.rb
@@ -22,12 +22,13 @@ module Gitlab
def initialize(metric_definition)
super
- type = options[:type]
+ return if options[:type]
- return if type.in?(allowed_types)
+ raise ArgumentError, "'type' option is required"
+ end
- prefix = "Invalid type #{type}. " if type
- raise ArgumentError, "#{prefix}Type must be one of: #{allowed_types.join(', ')}"
+ def available?
+ options[:type].in?(allowed_types)
end
private
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 16d85504dd1..686690eae40 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -33,59 +33,6 @@ module Gitlab
recorded_at
].freeze
- MIGRATED_INTEGRATIONS = %w[
- amazon_q
- apple_app_store
- asana
- assembla
- bamboo
- beyond_identity
- bugzilla
- buildkite
- campfire
- clickup
- confluence
- custom_issue_tracker
- datadog
- diffblue_cover
- discord
- drone_ci
- emails_on_push
- ewm
- external_wiki
- git_guardian
- github
- gitlab_slack_application
- google_play
- hangouts_chat
- harbor
- irker
- jenkins
- jira
- jira_cloud_app
- matrix
- mattermost
- mattermost_slash_commands
- microsoft_teams
- packagist
- phorge
- pipelines_email
- pivotaltracker
- prometheus
- pumble
- pushover
- redmine
- slack
- slack_slash_commands
- squash_tm
- teamcity
- telegram
- unify_circuit
- webex_teams
- youtrack
- zentao
- ].freeze
-
class << self
include Gitlab::Utils::UsageData
include Gitlab::Utils::StrongMemoize
@@ -175,7 +122,6 @@ module Gitlab
merge_requests: count(MergeRequest),
notes: count(Note)
}.merge(
- integrations_usage,
user_preferences_usage,
service_desk_counts
)
@@ -267,31 +213,6 @@ module Gitlab
Gitlab::UsageData::Topology.new.topology_usage_data
end
- # rubocop: disable CodeReuse/ActiveRecord
- def integrations_usage
- # rubocop: disable UsageData/LargeTable:
- available_integrations.each_with_object({}) do |name, response|
- next if MIGRATED_INTEGRATIONS.include?(name)
-
- type = Integration.integration_name_to_type(name)
-
- response[:"projects_#{name}_active"] = count(Integration.active.where.not(project: nil).where(type: type))
- response[:"groups_#{name}_active"] = count(Integration.active.where.not(group: nil).where(type: type))
- response[:"instances_#{name}_active"] = count(Integration.active.where(instance: true, type: type))
- response[:"projects_inheriting_#{name}_active"] = count(Integration.active.where.not(project: nil).where.not(inherit_from_id: nil).where(type: type))
- response[:"groups_inheriting_#{name}_active"] = count(Integration.active.where.not(group: nil).where.not(inherit_from_id: nil).where(type: type))
- end
- end
-
- def successful_deployments_with_cluster(scope)
- scope
- .joins(cluster: :deployments)
- .merge(::Clusters::Cluster.enabled)
- .merge(Deployment.success)
- end
- # rubocop: enable UsageData/LargeTable
- # rubocop: enable CodeReuse/ActiveRecord
-
# augmented in EE
def user_preferences_usage
{
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cd46702aaf3..edb1d6809a3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14586,6 +14586,9 @@ msgstr ""
msgid "CodeownersValidation|Entries with spaces"
msgstr ""
+msgid "CodeownersValidation|Group has no members with permission to approve merge requests"
+msgstr ""
+
msgid "CodeownersValidation|Group needs at least Developer access to the project for its members to approve merge requests"
msgstr ""
@@ -28593,6 +28596,12 @@ msgstr ""
msgid "GlobalSearch|Search labels"
msgstr ""
+msgid "GlobalSearch|Search or go to… %{kbdStart}/%{kbdEnd}"
+msgstr ""
+
+msgid "GlobalSearch|Search or go to… (or use the / keyboard shortcut)"
+msgstr ""
+
msgid "GlobalSearch|Search results are loading"
msgstr ""
@@ -56242,6 +56251,9 @@ msgstr ""
msgid "ServiceAccounts|Name"
msgstr ""
+msgid "ServiceAccounts|No service accounts"
+msgstr ""
+
msgid "ServiceAccounts|Service Accounts"
msgstr ""
@@ -66699,6 +66711,9 @@ msgstr ""
msgid "Vulnerability|Additional Info"
msgstr ""
+msgid "Vulnerability|Archive pending"
+msgstr ""
+
msgid "Vulnerability|Assert:"
msgstr ""
@@ -66912,6 +66927,9 @@ msgstr ""
msgid "Vulnerability|Vulnerability identifiers"
msgstr ""
+msgid "Vulnerability|Vulnerability will be archived on %{date}. %{linkStart}Why will this vulnerability be archived?%{linkEnd}"
+msgstr ""
+
msgid "Vulnerability|Vulnerable class:"
msgstr ""
diff --git a/package.json b/package.json
index b6281028fbe..fc4525377e3 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,7 @@
"@gitlab/application-sdk-browser": "^0.3.4",
"@gitlab/at.js": "1.5.7",
"@gitlab/cluster-client": "^2.5.0",
- "@gitlab/duo-ui": "^8.9.1",
+ "@gitlab/duo-ui": "^8.10.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/query-language-rust": "0.5.2",
@@ -295,7 +295,7 @@
"jest-jasmine2": "^29.7.0",
"jest-junit": "^12.3.0",
"jest-util": "^29.7.0",
- "markdownlint-cli2": "^0.17.1",
+ "markdownlint-cli2": "^0.17.2",
"miragejs": "^0.1.40",
"mock-apollo-client": "1.2.0",
"nodemon": "^2.0.19",
@@ -306,7 +306,7 @@
"swagger-cli": "^4.0.4",
"tailwindcss": "^3.4.1",
"timezone-mock": "^1.0.8",
- "vite": "^6.2.5",
+ "vite": "^6.2.6",
"vite-plugin-ruby": "^5.1.1",
"vue-loader-vue3": "npm:vue-loader@17.4.2",
"vue-test-utils-compat": "0.0.14",
diff --git a/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb
index 3129d10e894..510caf8adaf 100644
--- a/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb
@@ -21,12 +21,11 @@ module QA
end
let(:runner) do
- Resource::Ci::ProjectRunner.fabricate! do |runner|
- runner.name = "qa-runner-#{SecureRandom.hex(6)}"
- runner.tags = ["runner-for-#{imported_project.name}"]
- runner.executor = :docker
- runner.project = imported_project
- end
+ create(:project_runner,
+ name: "qa-runner-#{SecureRandom.hex(6)}",
+ tags: ["runner-for-#{imported_project.name}"],
+ executor: :docker,
+ project: imported_project)
end
before do
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index 75a64ca761a..2173d8759bc 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -61,7 +61,7 @@ then
# shellcheck disable=2059
printf "${COLOR_RED}ERROR: The number of README.md files has changed!${COLOR_RESET} Use index.md instead of README.md.\n" >&2
printf "If removing a README.md file, update NUMBER_READMES in lint-doc.sh.\n" >&2
- printf "https://docs.gitlab.com/ee/development/documentation/site_architecture/folder_structure.html#work-with-directories-and-files\n"
+ printf "https://docs.gitlab.com/development/documentation/site_architecture/folder_structure/#work-with-directories-and-files\n"
((ERRORCODE++))
fi
@@ -76,7 +76,7 @@ then
# shellcheck disable=2059
printf "${COLOR_RED}ERROR: The number of directory names containing dashes has changed!${COLOR_RESET} Use underscores instead of dashes for the directory names.\n" >&2
printf "If removing a directory containing dashes, update NUMBER_DASHES in lint-doc.sh.\n" >&2
- printf "https://docs.gitlab.com/ee/development/documentation/site_architecture/folder_structure.html#work-with-directories-and-files\n"
+ printf "https://docs.gitlab.com/development/documentation/site_architecture/folder_structure/#work-with-directories-and-files\n"
((ERRORCODE++))
fi
@@ -91,7 +91,7 @@ then
# shellcheck disable=2059
printf "${COLOR_RED}ERROR: The number of filenames containing dashes has changed!${COLOR_RESET} Use underscores instead of dashes for the filenames.\n" >&2
printf "If removing a file containing dashes, update the filename NUMBER_DASHES in lint-doc.sh.\n" >&2
- printf "https://docs.gitlab.com/ee/development/documentation/site_architecture/folder_structure.html#work-with-directories-and-files\n"
+ printf "https://docs.gitlab.com/development/documentation/site_architecture/folder_structure/#work-with-directories-and-files\n"
((ERRORCODE++))
fi
@@ -104,7 +104,7 @@ if echo "${FIND_UPPERCASE_DIRS}" | grep . &>/dev/null
then
# shellcheck disable=2059
printf "${COLOR_RED}ERROR: Found one or more directories with an uppercase letter in their name!${COLOR_RESET} Use lowercase instead of uppercase for the directory names.\n" >&2
- printf "https://docs.gitlab.com/ee/development/documentation/site_architecture/folder_structure.html#work-with-directories-and-files\n" >&2
+ printf "https://docs.gitlab.com/development/documentation/site_architecture/folder_structure/#work-with-directories-and-files\n" >&2
echo "${FIND_UPPERCASE_DIRS}"
((ERRORCODE++))
fi
@@ -133,23 +133,11 @@ if echo "${FIND_UPPERCASE_FILES}" | grep . &>/dev/null
then
# shellcheck disable=2059
printf "${COLOR_RED}ERROR: Found one or more file names with an uppercase letter in their name!${COLOR_RESET} Use lowercase instead of uppercase for the file names.\n" >&2
- printf "https://docs.gitlab.com/ee/development/documentation/site_architecture/folder_structure.html#work-with-directories-and-files\n" >&2
+ printf "https://docs.gitlab.com/development/documentation/site_architecture/folder_structure/#work-with-directories-and-files\n" >&2
echo "${FIND_UPPERCASE_FILES}"
((ERRORCODE++))
fi
-FIND_ALL_DOCS_DIRECTORIES=$(find doc -type d)
-# shellcheck disable=2059
-printf "${COLOR_GREEN}INFO: Checking for documentation path clashes...${COLOR_RESET}\n"
-for directory in $FIND_ALL_DOCS_DIRECTORIES; do
- # Markdown files should not have the same path as a directory with an index.md file in it
- if [[ -f "${directory}.md" ]] && [[ -f "${directory}/index.md" ]]; then
- # shellcheck disable=2059
- printf "${COLOR_YELLOW}WARNING: File ${directory}.md clashes with file ${directory}/index.md!${COLOR_RESET} "
- printf "For more information, see https://gitlab.com/gitlab-org/gitlab-docs/-/issues/1792.\n"
- fi
-done
-
# Run Vale and Markdownlint only on changed files. Only works on merged results
# pipelines, so first checks if a merged results CI variable is present. If not present,
# runs test on all files.
@@ -195,7 +183,7 @@ function run_locally_or_in_container() {
local cmd=$1
local args=$2
local files=$3
- local registry_url="registry.gitlab.com/gitlab-org/technical-writing/docs-gitlab-com/lint-markdown:alpine-3.21-vale-3.9.3-markdownlint2-0.17.1-lychee-0.18.0"
+ local registry_url="registry.gitlab.com/gitlab-org/technical-writing/docs-gitlab-com/lint-markdown:alpine-3.21-vale-3.11.2-markdownlint2-0.17.2-lychee-0.18.1"
if hash "${cmd}" 2>/dev/null
then
diff --git a/spec/components/rapid_diffs/app_component_spec.rb b/spec/components/rapid_diffs/app_component_spec.rb
index ad541a351ef..866f0df5e73 100644
--- a/spec/components/rapid_diffs/app_component_spec.rb
+++ b/spec/components/rapid_diffs/app_component_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe RapidDiffs::AppComponent, type: :component, feature_category: :co
let(:show_whitespace) { true }
let(:diff_view) { 'inline' }
let(:update_user_endpoint) { '/update_user' }
- let(:metadata_endpoint) { '/metadata' }
+ let(:diffs_stats_endpoint) { '/diffs_stats' }
let(:diff_files_endpoint) { '/diff_files_metadata' }
it "renders diffs slice" do
@@ -22,7 +22,7 @@ RSpec.describe RapidDiffs::AppComponent, type: :component, feature_category: :co
app = page.find('[data-rapid-diffs]')
expect(app).not_to be_nil
expect(app['data-reload-stream-url']).to eq(reload_stream_url)
- expect(app['data-metadata-endpoint']).to eq(metadata_endpoint)
+ expect(app['data-diffs-stats-endpoint']).to eq(diffs_stats_endpoint)
expect(app['data-diff-files-endpoint']).to eq(diff_files_endpoint)
end
@@ -86,7 +86,8 @@ RSpec.describe RapidDiffs::AppComponent, type: :component, feature_category: :co
it 'preloads' do
instance = create_instance
render_inline(instance)
- expect(instance.helpers.page_startup_api_calls).to include(metadata_endpoint)
+ expect(instance.helpers.page_startup_api_calls).to include(diffs_stats_endpoint)
+ expect(instance.helpers.page_startup_api_calls).to include(diff_files_endpoint)
expect(vc_test_controller.view_context.content_for?(:startup_js)).not_to be_nil
end
@@ -113,7 +114,7 @@ RSpec.describe RapidDiffs::AppComponent, type: :component, feature_category: :co
show_whitespace:,
diff_view:,
update_user_endpoint:,
- metadata_endpoint:,
+ diffs_stats_endpoint:,
diff_files_endpoint:,
lazy:
)
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index 3a1d4ac96fc..e1b02c8d752 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'Global search', :js, feature_category: :global_search do
shared_examples 'header search' do
it 'renders search button' do
- expect(page).to have_button('Search or go to…')
+ expect(page).to have_selector('[data-testid="super-sidebar-search-button"]')
end
it 'opens search modal when shortcut "s" is pressed' do
diff --git a/spec/features/projects/files/file_open_mrs_dropdown_spec.rb b/spec/features/projects/files/file_open_mrs_dropdown_spec.rb
new file mode 100644
index 00000000000..c2c29462956
--- /dev/null
+++ b/spec/features/projects/files/file_open_mrs_dropdown_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Projects > Files > Open MRs dropdown', :js, feature_category: :source_code_management do
+ include FilteredSearchHelpers
+
+ def create_mr(branch_name, title)
+ project.repository.create_branch(branch_name)
+
+ project.repository.commit_files(
+ user,
+ branch_name: branch_name,
+ message: "Update readme file",
+ actions: [{ action: :update, file_path: file_path, content: "Updated file content" }]
+ )
+
+ create(:merge_request, source_project: project, target_branch: target_branch, source_branch: branch_name,
+ title: title)
+ end
+
+ let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be(:user) { project.creator }
+ let_it_be(:file_path) { 'README.md' }
+ let_it_be(:mr_title) { 'Update README.md' }
+ let_it_be(:source_branch) { 'open-mrs-badge-test-1' }
+ let_it_be(:another_mr_title) { 'Second update to README.md' }
+ let_it_be(:another_source_branch) { 'open-mrs-badge-test-2' }
+ let_it_be(:target_branch) { 'master' }
+
+ before do
+ sign_in(user)
+
+ create_mr(source_branch, mr_title)
+ create_mr(another_source_branch, another_mr_title)
+ end
+
+ context 'when feature flags are enabled' do
+ before do
+ stub_feature_flags(
+ filter_blob_path: true,
+ blob_repository_vue_header_app: true
+ )
+ end
+
+ it 'shows correct count and lists all MRs in dropdown' do
+ visit project_blob_path(project, "master/#{file_path}")
+
+ badge = find_by_testid('open-mr-badge')
+ expect(badge).to have_content('2 Open')
+
+ badge.click
+ wait_for_requests
+
+ within_testid('disclosure-content') do
+ expect(page).to have_content(mr_title)
+ expect(page).to have_content(another_mr_title)
+
+ click_button mr_title
+ end
+
+ expect(page).to have_content(mr_title)
+ end
+ end
+
+ context 'when feature flags are disabled' do
+ before do
+ stub_feature_flags(
+ filter_blob_path: false,
+ blob_repository_vue_header_app: false
+ )
+ end
+
+ it 'does not display the open MRs badge' do
+ visit project_blob_path(project, "master/#{file_path}")
+
+ expect(page).not_to have_testid('open-mr-badge')
+ end
+ end
+end
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 4b770dbbc48..5e205c7495c 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe 'User uses header search field', :js, :disable_rate_limiter, feat
context 'when clicking the search button' do
before do
- click_button "Search or go to…"
+ find_by_testid('super-sidebar-search-button').click
wait_for_all_requests
end
diff --git a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
index 0967ebd4874..d2f1945208e 100644
--- a/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_instructions_spec.js
@@ -20,8 +20,8 @@ import {
DEFAULT_PLATFORM,
EXECUTORS_HELP_URL,
SERVICE_COMMANDS_HELP_URL,
- STATUS_NEVER_CONTACTED,
- STATUS_ONLINE,
+ CREATION_STATE_STARTED,
+ CREATION_STATE_FINISHED,
RUNNER_REGISTRATION_POLLING_INTERVAL_MS,
WINDOWS_PLATFORM,
GOOGLE_CLOUD_PLATFORM,
@@ -216,7 +216,7 @@ describe('RegistrationInstructions', () => {
it('when runner is online, stops polling and announces runner is registered', async () => {
expect(wrapper.emitted('runnerRegistered')).toBeUndefined();
- mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE });
+ mockResolvedRunner({ ...mockRunner, creationState: CREATION_STATE_FINISHED });
await waitForPolling();
expect(wrapper.emitted('runnerRegistered')).toHaveLength(1);
@@ -288,7 +288,7 @@ describe('RegistrationInstructions', () => {
await waitForPolling();
- mockResolvedRunner({ ...mockRunner, status: STATUS_NEVER_CONTACTED });
+ mockResolvedRunner({ ...mockRunner, creationState: CREATION_STATE_STARTED });
await waitForPolling();
});
@@ -317,7 +317,7 @@ describe('RegistrationInstructions', () => {
createComponent();
await waitForPolling();
- mockResolvedRunner({ ...mockRunner, status: STATUS_ONLINE });
+ mockResolvedRunner({ ...mockRunner, creationState: CREATION_STATE_FINISHED });
await waitForPolling();
});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 5e0045ea46a..498bf84424a 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -542,9 +542,11 @@ describe('diffs/components/app', () => {
describe('File browser', () => {
it('should render file browser when files are present', () => {
+ store.realSize = '20';
store.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
createComponent();
expect(wrapper.findComponent(DiffsFileTree).exists()).toBe(true);
+ expect(wrapper.findComponent(DiffsFileTree).props('totalFilesCount')).toBe('20');
});
it('should not render file browser without files', async () => {
diff --git a/spec/frontend/diffs/components/diffs_file_tree_spec.js b/spec/frontend/diffs/components/diffs_file_tree_spec.js
index 508aab30f08..981aa61a389 100644
--- a/spec/frontend/diffs/components/diffs_file_tree_spec.js
+++ b/spec/frontend/diffs/components/diffs_file_tree_spec.js
@@ -194,9 +194,11 @@ describe('DiffsFileTree', () => {
});
});
- it('passes down loadedFiles table to tree list', () => {
+ it('passes down props to tree list', () => {
const loadedFiles = { foo: true };
- createComponent({ loadedFiles });
+ const totalFilesCount = '20';
+ createComponent({ loadedFiles, totalFilesCount });
expect(wrapper.findComponent(TreeList).props('loadedFiles')).toBe(loadedFiles);
+ expect(wrapper.findComponent(TreeList).props('totalFilesCount')).toBe(totalFilesCount);
});
});
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index f6bdd0a8428..c6f18149082 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -53,7 +53,6 @@ describe('Diffs tree list component', () => {
useLegacyDiffs().addedLines = 10;
useLegacyDiffs().addedLines = 20;
useLegacyDiffs().mergeRequestDiff = {};
- useLegacyDiffs().realSize = '20';
useLegacyDiffs().setTreeOpen.mockReturnValue();
});
@@ -165,7 +164,7 @@ describe('Diffs tree list component', () => {
});
it('renders file count', () => {
- createComponent();
+ createComponent({ totalFilesCount: '20' });
expect(wrapper.findByTestId('file-count').text()).toBe('20');
});
diff --git a/spec/frontend/rapid_diffs/app/app_spec.js b/spec/frontend/rapid_diffs/app/app_spec.js
index c663b44928e..aa4e61236e3 100644
--- a/spec/frontend/rapid_diffs/app/app_spec.js
+++ b/spec/frontend/rapid_diffs/app/app_spec.js
@@ -28,19 +28,6 @@ describe('Rapid Diffs App', () => {
app = createRapidDiffsApp(options);
};
- beforeEach(() => {
- createTestingPinia();
- useDiffsView(pinia).loadMetadata.mockResolvedValue();
- initFileBrowser.mockResolvedValue();
- setHTMLFixture(
- `
-
- `,
- );
- });
-
beforeAll(() => {
Object.defineProperty(window, 'customElements', {
value: { define: jest.fn() },
@@ -48,23 +35,28 @@ describe('Rapid Diffs App', () => {
});
});
- it('initializes the app', async () => {
- let res;
- const mock = useDiffsView().loadMetadata.mockImplementationOnce(
- () =>
- new Promise((resolve) => {
- res = resolve;
- }),
+ beforeEach(() => {
+ createTestingPinia();
+ useDiffsView().loadDiffsStats.mockResolvedValue();
+ initFileBrowser.mockResolvedValue();
+ setHTMLFixture(
+ `
+
+ `,
);
+ });
+
+ it('initializes the app', () => {
createApp();
app.init();
- expect(useDiffsView().metadataEndpoint).toBe('/metadata');
- expect(mock).toHaveBeenCalled();
+ expect(useDiffsView().diffsStatsEndpoint).toBe('/stats');
+ expect(useDiffsView().loadDiffsStats).toHaveBeenCalled();
expect(initViewSettings).toHaveBeenCalledWith({ pinia, streamUrl: '/reload' });
expect(window.customElements.define).toHaveBeenCalledWith('diff-file', DiffFile);
expect(window.customElements.define).toHaveBeenCalledWith('diff-file-mounted', DiffFileMounted);
expect(window.customElements.define).toHaveBeenCalledWith('streaming-error', StreamingError);
- await res();
expect(initHiddenFilesWarning).toHaveBeenCalled();
expect(fixWebComponentsStreamingOnSafari).toHaveBeenCalled();
expect(initFileBrowser).toHaveBeenCalledWith('/diff-files-metadata');
diff --git a/spec/frontend/rapid_diffs/app/file_browser_spec.js b/spec/frontend/rapid_diffs/app/file_browser_spec.js
index 53adb68be30..9e734b6ac52 100644
--- a/spec/frontend/rapid_diffs/app/file_browser_spec.js
+++ b/spec/frontend/rapid_diffs/app/file_browser_spec.js
@@ -1,34 +1,44 @@
import { shallowMount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import { PiniaVuePlugin } from 'pinia';
import FileBrowser from '~/rapid_diffs/app/file_browser.vue';
import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
import store from '~/mr_notes/stores';
import { useDiffsList } from '~/rapid_diffs/stores/diffs_list';
import { useFileBrowser } from '~/diffs/stores/file_browser';
+import { useDiffsView } from '~/rapid_diffs/stores/diffs_view';
Vue.use(PiniaVuePlugin);
describe('FileBrowser', () => {
let wrapper;
+ let pinia;
const createComponent = () => {
- const pinia = createTestingPinia();
- useDiffsList();
- useFileBrowser();
wrapper = shallowMount(FileBrowser, {
store,
pinia,
});
};
- it('passes down loaded files', async () => {
+ beforeEach(() => {
+ pinia = createTestingPinia();
+ useDiffsList();
+ useDiffsView();
+ useFileBrowser();
+ });
+
+ it('passes down props', () => {
const loadedFiles = { foo: 1 };
- createComponent();
+ const totalFilesCount = 20;
useDiffsList().loadedFiles = loadedFiles;
- await nextTick();
- expect(wrapper.findComponent(DiffsFileTree).props('loadedFiles')).toStrictEqual(loadedFiles);
+ useDiffsView().diffsStats = { diffsCount: totalFilesCount };
+ createComponent();
+ const tree = wrapper.findComponent(DiffsFileTree);
+ expect(tree.props('loadedFiles')).toStrictEqual(loadedFiles);
+ expect(tree.props('totalFilesCount')).toStrictEqual(totalFilesCount);
+ expect(tree.props('floatingResize')).toBe(true);
});
it('uses floating resize', () => {
@@ -41,10 +51,9 @@ describe('FileBrowser', () => {
expect(wrapper.findComponent(DiffsFileTree).exists()).toBe(true);
});
- it('hides file browser', async () => {
- createComponent();
+ it('hides file browser', () => {
useFileBrowser().fileBrowserVisible = false;
- await nextTick();
+ createComponent();
expect(wrapper.findComponent(DiffsFileTree).exists()).toBe(false);
});
diff --git a/spec/frontend/rapid_diffs/app/view_settings_spec.js b/spec/frontend/rapid_diffs/app/view_settings_spec.js
index 865ed154d2b..71ef32ffe9e 100644
--- a/spec/frontend/rapid_diffs/app/view_settings_spec.js
+++ b/spec/frontend/rapid_diffs/app/view_settings_spec.js
@@ -71,7 +71,7 @@ describe('View settings', () => {
it('sets diff app controls props', () => {
useDiffsList().loadedFiles = { foo: true };
- useDiffsView().diffStats = {
+ useDiffsView().diffsStats = {
addedLines: 1,
removedLines: 2,
diffsCount: 3,
diff --git a/spec/frontend/rapid_diffs/stores/diffs_view_spec.js b/spec/frontend/rapid_diffs/stores/diffs_view_spec.js
index e52aa8c7d0c..c88c77e5ea7 100644
--- a/spec/frontend/rapid_diffs/stores/diffs_view_spec.js
+++ b/spec/frontend/rapid_diffs/stores/diffs_view_spec.js
@@ -14,7 +14,6 @@ import {
import { queueRedisHllEvents } from '~/diffs/utils/queue_events';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import vuexStore from '~/mr_notes/stores';
const defaultState = {
updateUserEndpoint: '/update',
@@ -40,39 +39,52 @@ describe('Diffs view store', () => {
setActivePinia(pinia);
store = useDiffsView();
useDiffsList().reloadDiffs.mockResolvedValue();
- jest.spyOn(vuexStore, 'dispatch').mockResolvedValue();
});
- describe('#loadMetadata', () => {
- it('uses Vuex store to load metadata', () => {
- const spy = jest.spyOn(vuexStore, 'dispatch');
- store.metadataEndpoint = '/metadata';
- store.loadMetadata();
- expect(vuexStore.state.diffs.endpointMetadata).toBe('/metadata');
- expect(vuexStore.state.diffs.diffViewType).toBe('inline');
- expect(vuexStore.state.diffs.showWhitespace).toBe(true);
- expect(spy).toHaveBeenCalledWith('diffs/fetchDiffFilesMeta');
+ describe('#loadDiffsStats', () => {
+ const endpoint = '/stats';
+
+ beforeEach(() => {
+ store.diffsStatsEndpoint = endpoint;
});
- it('copies values from Vuex store for diff stats', async () => {
- vuexStore.state.diffs.addedLines = 1;
- vuexStore.state.diffs.removedLines = 2;
- vuexStore.state.diffs.realSize = '3';
- vuexStore.state.diffs.size = 2;
- vuexStore.state.diffs.plainDiffPath = 'plain/diffs';
- vuexStore.state.diffs.emailPatchPath = 'email/patch';
- vuexStore.state.diffs.renderOverflowWarning = true;
- await store.loadMetadata();
- expect(store.diffStats).toStrictEqual({
- addedLines: 1,
- removedLines: 2,
- diffsCount: 3,
- realSize: '3',
- size: 2,
- plainDiffPath: 'plain/diffs',
- emailPatchPath: 'email/patch',
- renderOverflowWarning: true,
+ it('loads diff stats', async () => {
+ const addedLines = 10;
+ const removedLines = 20;
+ const diffsCount = 5;
+ mockAxios.onGet(endpoint).reply(HTTP_STATUS_OK, {
+ diffs_stats: {
+ added_lines: addedLines,
+ removed_lines: removedLines,
+ diffs_count: diffsCount,
+ },
});
+ await store.loadDiffsStats();
+ expect(store.diffsStats).toEqual({ addedLines, removedLines, diffsCount });
+ expect(store.overflow).toBe(null);
+ });
+
+ it('sets overflow', async () => {
+ const addedLines = 10;
+ const removedLines = 20;
+ const diffsCount = 500;
+ const visibleCount = 50;
+ const emailPath = '/email';
+ const diffPath = '/diff';
+ mockAxios.onGet(endpoint).reply(HTTP_STATUS_OK, {
+ diffs_stats: {
+ added_lines: addedLines,
+ removed_lines: removedLines,
+ diffs_count: diffsCount,
+ },
+ overflow: {
+ visible_count: visibleCount,
+ email_path: emailPath,
+ diff_path: diffPath,
+ },
+ });
+ await store.loadDiffsStats();
+ expect(store.overflow).toEqual({ visibleCount, emailPath, diffPath });
});
});
@@ -139,4 +151,11 @@ describe('Diffs view store', () => {
).toBe(true);
});
});
+
+ describe('#totalFilesCount', () => {
+ it('returns diffs count', () => {
+ store.diffsStats = { diffsCount: 10 };
+ expect(store.totalFilesCount).toBe(10);
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_header_app_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_header_app_spec.js
index aee2c902e31..074a89e550c 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_header_app_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_header_app_spec.js
@@ -1,7 +1,5 @@
-import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import GlobalSearchHeaderApp from '~/super_sidebar/components/global_search/components/global_search_header_app.vue';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
import waitForPromises from 'helpers/wait_for_promises';
import SearchModal from '~/super_sidebar/components/global_search/components/global_search.vue';
@@ -17,9 +15,6 @@ describe('GlobalSearchHeaderApp', () => {
const createComponent = ({ features = { searchButtonTopRight: true } } = {}) => {
wrapper = shallowMountExtended(GlobalSearchHeaderApp, {
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
provide: {
glFeatures: {
...features,
@@ -43,11 +38,6 @@ describe('GlobalSearchHeaderApp', () => {
expect(findSearchButton().exists()).toBe(true);
});
- it('search button should have tooltip', () => {
- const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
- expect(tooltip.value).toBe(`Type / to search`);
- });
-
it('search button should have tracking', async () => {
const { trackEventSpy } = bindInternalEventDocument(findSearchButton().element);
await findSearchButton().vm.$emit('click');
@@ -63,22 +53,6 @@ describe('GlobalSearchHeaderApp', () => {
expect(findSearchModal().exists()).toBe(true);
});
- describe('Search tooltip', () => {
- it('should hide search tooltip when modal is shown', async () => {
- findSearchModal().vm.$emit('shown');
- await nextTick();
- const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
- expect(tooltip.value).toBe('');
- });
-
- it('should add search tooltip when modal is hidden', async () => {
- findSearchModal().vm.$emit('hidden');
- await nextTick();
- const tooltip = getBinding(findSearchButton().element, 'gl-tooltip');
- expect(tooltip.value).toBe(`Type / to search`);
- });
- });
-
describe('when feature flag is off', () => {
beforeEach(() => {
createComponent({ features: { searchButtonTopRight: false } });
diff --git a/spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb b/spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb
index 493e628ac83..40daa84a8d9 100644
--- a/spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb
+++ b/spec/graphql/types/work_items/widgets/award_emoji_type_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Types::WorkItems::Widgets::AwardEmojiType, feature_category: :team_planning do
it 'exposes the expected fields' do
- expected_fields = %i[award_emoji downvotes upvotes type]
+ expected_fields = %i[award_emoji downvotes new_custom_emoji_path upvotes type]
expect(described_class.graphql_name).to eq('WorkItemWidgetAwardEmoji')
expect(described_class).to have_graphql_fields(*expected_fields)
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 8f21d9d28c8..7cfe433a7c5 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -760,6 +760,33 @@ RSpec.describe ApplicationHelper do
end
end
+ describe '#disable_fixed_body_scroll' do
+ it 'disables body scroll' do
+ helper.disable_fixed_body_scroll
+ expect(helper.body_scroll_classes).to eq('')
+ end
+ end
+
+ describe '#body_scroll_classes' do
+ before do
+ stub_feature_flags(force_scrollbar: true)
+ end
+
+ it 'fixes body scroll by default' do
+ expect(helper.body_scroll_classes).to eq('body-fixed-scrollbar')
+ end
+
+ context 'with feature disabled' do
+ before do
+ stub_feature_flags(force_scrollbar: false)
+ end
+
+ it 'does nothing' do
+ expect(helper.body_scroll_classes).to eq('')
+ end
+ end
+ end
+
describe '#dispensable_render' do
context 'when an error occurs in the template to be rendered' do
before do
diff --git a/spec/lib/gitlab/ci/build/prerequisite/managed_resource_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/managed_resource_spec.rb
index ee80fb24f82..30e82283c28 100644
--- a/spec/lib/gitlab/ci/build/prerequisite/managed_resource_spec.rb
+++ b/spec/lib/gitlab/ci/build/prerequisite/managed_resource_spec.rb
@@ -135,6 +135,13 @@ RSpec.describe Gitlab::Ci::Build::Prerequisite::ManagedResource, feature_categor
create(:ci_build, environment: environment, user: user, deployment: deployment, project: deployment_project)
end
+ let(:default_template) do
+ Gitlab::Agent::ManagedResources::EnvironmentTemplate.new(
+ name: 'default',
+ data: { objects: [], delete_resources: 'on_stop' }.stringify_keys.to_yaml
+ )
+ end
+
let(:instance) { described_class.new(build) }
subject(:execute_complete) { instance.complete! }
@@ -210,14 +217,13 @@ RSpec.describe Gitlab::Ci::Build::Prerequisite::ManagedResource, feature_categor
allow(kas_client).to receive_messages(get_environment_template: double,
render_environment_template: double)
- template = double
allow(kas_client).to receive(:get_environment_template)
.with(agent: cluster_agent, template_name: 'default')
- .and_return(template)
+ .and_return(default_template)
rendered_template = double
allow(kas_client).to receive(:render_environment_template)
- .with(template: template, environment: environment, build: build)
+ .with(template: default_template, environment: environment, build: build)
.and_return(rendered_template)
allow(kas_client).to receive(:ensure_environment)
@@ -230,6 +236,8 @@ RSpec.describe Gitlab::Ci::Build::Prerequisite::ManagedResource, feature_categor
expect { execute_complete }.to change { Clusters::Agents::ManagedResource.count }.by(1)
managed_resource = Clusters::Agents::ManagedResource.find_by_build_id(build.id)
expect(managed_resource.status).to eq("completed")
+ expect(managed_resource.template_name).to eq('default')
+ expect(managed_resource.deletion_strategy).to eq('on_stop')
expect(managed_resource.tracked_objects).to contain_exactly(namespace_attributes, role_binding_attributes)
end
@@ -247,7 +255,7 @@ RSpec.describe Gitlab::Ci::Build::Prerequisite::ManagedResource, feature_categor
before do
allow_next_instance_of(Gitlab::Kas::Client) do |kas_client|
allow(kas_client).to receive(:get_environment_template).and_raise(GRPC::NotFound)
- allow(kas_client).to receive_messages(get_default_environment_template: double,
+ allow(kas_client).to receive_messages(get_default_environment_template: default_template,
render_environment_template: double)
success_response = Gitlab::Agent::ManagedResources::Rpc::EnsureEnvironmentResponse.new(errors: [])
allow(kas_client).to receive(:ensure_environment).and_return(success_response)
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
index c5fde66df13..ae51f9f372d 100644
--- a/spec/lib/gitlab/omniauth_initializer_spec.rb
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -227,13 +227,23 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
end
context 'when SAML providers are configured' do
+ let(:base_url) { 'https://example.com' }
+
+ before do
+ allow(described_class).to receive(:full_host).and_return(base_url.to_s)
+ end
+
it 'configures default args for a single SAML provider' do
- stub_omniauth_config(providers: [{ name: 'saml', args: { idp_sso_service_url: 'https://saml.example.com' } }])
+ stub_omniauth_config(providers: [{ name: 'saml', args: {
+ idp_sso_service_url: 'https://saml.example.com',
+ assertion_consumer_service_url: "#{base_url}/users/auth/saml/callback"
+ } }])
expect(devise_config).to receive(:omniauth).with(
:saml,
{
idp_sso_service_url: 'https://saml.example.com',
+ assertion_consumer_service_url: "#{base_url}/users/auth/saml/callback",
attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
}
)
@@ -247,7 +257,11 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
providers: [
{
name: 'saml',
- args: { idp_sso_service_url: 'https://saml.example.com', attribute_statements: { email: ['custom_attr'] } }
+ args: {
+ assertion_consumer_service_url: "https://saml.example.com/users/auth/saml/callback",
+ idp_sso_service_url: 'https://saml.example.com',
+ attribute_statements: { email: ['custom_attr'] }
+ }
}
]
)
@@ -258,6 +272,7 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
:saml,
{
idp_sso_service_url: 'https://saml.example.com',
+ assertion_consumer_service_url: "https://saml.example.com/users/auth/saml/callback",
attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
.merge({ email: ['custom_attr'] })
}
@@ -273,6 +288,7 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
:saml,
{
idp_sso_service_url: 'https://saml.example.com',
+ assertion_consumer_service_url: "#{base_url}/users/auth/saml/callback",
attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
}
)
@@ -303,6 +319,7 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
expect(devise_config).to receive(:omniauth).with(
:saml,
{
+ assertion_consumer_service_url: "#{base_url}/users/auth/saml/callback",
attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements,
idp_cert_fingerprint: "DD:80:B1:FA:A9:A7:8D:9D:41:7E:09:10:D8:6F:7D:0A:7E:58:4C:C4",
idp_cert_fingerprint_algorithm: "http://www.w3.org/2000/09/xmldsig#sha1"
@@ -320,6 +337,7 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
expect(devise_config).to receive(:omniauth).with(
:saml,
{
+ assertion_consumer_service_url: "#{base_url}/users/auth/saml/callback",
attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements,
idp_cert_fingerprint:
"73:2D:28:C2:D2:D0:34:9F:F8:9A:9C:74:23:BF:0A:CB:66:75:78:9B:01:4D:1F:7D:60:8F:AD:47:A2:30:D7:4A",
@@ -347,6 +365,7 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
expect(devise_config).to receive(:omniauth).with(
:saml,
{
+ assertion_consumer_service_url: "#{base_url}/users/auth/saml/callback",
attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements,
idp_cert_fingerprint: "DD:80:B1:FA:A9:A7:8D:9D:41:7E:09:10:D8:6F:7D:0A:7E:58:4C:C4",
idp_cert_fingerprint_algorithm: "http://www.w3.org/2001/04/xmlenc#sha256"
@@ -372,6 +391,7 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
:saml,
{
idp_sso_service_url: 'https://saml.example.com',
+ assertion_consumer_service_url: "#{base_url}/users/auth/saml/callback",
attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
}
)
@@ -380,6 +400,7 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
{
idp_sso_service_url: 'https://saml2.example.com',
strategy_class: OmniAuth::Strategies::SAML,
+ assertion_consumer_service_url: "#{base_url}/users/auth/saml2/callback",
attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
}
)
@@ -394,6 +415,7 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
name: 'custom_saml',
args: {
strategy_class: 'OmniAuth::Strategies::SAML',
+ assertion_consumer_service_url: "https://saml2.example.com/users/auth/custom_saml/callback",
idp_sso_service_url: 'https://saml2.example.com',
attribute_statements: { email: ['custom_attr'] }
}
@@ -406,6 +428,7 @@ RSpec.describe Gitlab::OmniauthInitializer, feature_category: :system_access do
{
idp_sso_service_url: 'https://saml2.example.com',
strategy_class: OmniAuth::Strategies::SAML,
+ assertion_consumer_service_url: "https://saml2.example.com/users/auth/custom_saml/callback",
attribute_statements: ::Gitlab::Auth::Saml::Config.default_attribute_statements
.merge({ email: ['custom_attr'] })
}
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/base_integrations_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/base_integrations_metric_spec.rb
index fc409919278..35142441b60 100644
--- a/spec/lib/gitlab/usage/metrics/instrumentations/base_integrations_metric_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/instrumentations/base_integrations_metric_spec.rb
@@ -7,18 +7,16 @@ RSpec.describe Gitlab::Usage::Metrics::Instrumentations::BaseIntegrationsMetric,
it "raises an exception if options are not present" do
expect do
described_class.new(options: {}, time_frame: 'all')
- end.to raise_error(ArgumentError, %r{^Type must be one of})
+ end.to raise_error(ArgumentError, %r{^'type' option is required})
end
- it "raises an exception if options have an invalid value" do
- expect do
- described_class.new(options: { type: 'blahblah' }, time_frame: 'all')
- end.to raise_error(ArgumentError, %r{^Invalid type blahblah. Type must be one of})
+ it "is not available if options have an invalid value" do
+ available = described_class.new(options: { type: 'blahblah' }, time_frame: 'all').available?
+ expect(available).to be(false)
end
- it "raises no exceptions when options have a valid value" do
- expect do
- described_class.new(options: { type: 'pivotaltracker' }, time_frame: 'all')
- end.not_to raise_error
+ it "is available when options have a valid value" do
+ available = described_class.new(options: { type: 'pivotaltracker' }, time_frame: 'all').available?
+ expect(available).to be(true)
end
end
diff --git a/spec/models/ci/job_token/authorization_spec.rb b/spec/models/ci/job_token/authorization_spec.rb
index 10b9d53e39c..e697461e442 100644
--- a/spec/models/ci/job_token/authorization_spec.rb
+++ b/spec/models/ci/job_token/authorization_spec.rb
@@ -81,11 +81,11 @@ RSpec.describe Ci::JobToken::Authorization, feature_category: :secrets_managemen
accessed_project_id: accessed_project.id)
end
- it_behaves_like 'internal event tracking' do
- let(:event) { 'authorize_job_token_with_disabled_scope' }
- let(:project) { accessed_project }
- let(:category) { described_class }
- let(:label) { 'cross-project' }
+ it "triggers an internal event" do
+ expect { capture }.to trigger_internal_events('authorize_job_token_with_disabled_scope').with(
+ project: accessed_project,
+ additional_properties: { label: 'cross-project' }
+ )
end
context 'when origin project is the same as the accessed project' do
@@ -96,11 +96,11 @@ RSpec.describe Ci::JobToken::Authorization, feature_category: :secrets_managemen
expect(described_class.captured_authorizations).to be_nil
end
- it_behaves_like 'internal event tracking' do
- let(:event) { 'authorize_job_token_with_disabled_scope' }
- let(:project) { accessed_project }
- let(:category) { described_class }
- let(:label) { 'same-project' }
+ it "triggers an internal event" do
+ expect { capture }.to trigger_internal_events('authorize_job_token_with_disabled_scope').with(
+ project: accessed_project,
+ additional_properties: { label: 'same-project' }
+ )
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 6249f341c71..2d55e89a11b 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -272,12 +272,13 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
cancel!: 'canceled'
}.each do |pipeline_event, status|
context "when transitioning to #{status}" do
- it_behaves_like 'internal event tracking' do
- let(:event) { 'completed_pipeline_execution' }
- let(:additional_properties) { { label: status } }
- let(:category) { described_class.name }
-
- subject(:completed_pipeline) { pipeline.public_send(pipeline_event) }
+ it "triggers an internal event" do
+ expect { pipeline.public_send(pipeline_event) }.to trigger_internal_events('completed_pipeline_execution')
+ .with(
+ project: project,
+ user: user,
+ additional_properties: { label: status }
+ )
end
end
end
@@ -288,23 +289,20 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
describe '.track_ci_pipeline_created_event' do
let(:pipeline) { build(:ci_pipeline, user: user) }
- it_behaves_like 'internal event tracking' do
- let(:event) { 'create_ci_internal_pipeline' }
- let(:additional_properties) do
- {
- label: 'push',
- property: 'unknown_source'
- }
- end
-
- subject { pipeline.save! }
+ it "triggers an internal event" do
+ expect { pipeline.save! }.to trigger_internal_events('create_ci_internal_pipeline').with(
+ category: 'InternalEventTracking',
+ project: project,
+ user: user,
+ additional_properties: { label: 'push', property: 'unknown_source' }
+ )
end
context 'when pipeline is external' do
let(:pipeline) { build(:ci_pipeline, source: :external) }
- it_behaves_like 'internal event not tracked' do
- subject { pipeline.save! }
+ it "doesn't trigger an internal event" do
+ expect { pipeline.save! }.to not_trigger_internal_events('create_ci_internal_pipeline')
end
end
end
diff --git a/spec/models/ci/runner_manager_spec.rb b/spec/models/ci/runner_manager_spec.rb
index 4ba8bd99b0b..c20232ac185 100644
--- a/spec/models/ci/runner_manager_spec.rb
+++ b/spec/models/ci/runner_manager_spec.rb
@@ -458,17 +458,25 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo
end
describe '#status', :freeze_time, :clean_gitlab_redis_cache do
- subject { runner_manager.status }
+ let(:runner_manager) { build(:ci_runner_machine, *Array.wrap(traits)) }
- context 'if never connected' do
- let(:runner_manager) { build(:ci_runner_machine, :unregistered, :stale) }
+ subject(:status) { runner_manager.status }
- it { is_expected.to eq(:stale) }
+ context 'if unregistered' do
+ let(:traits) { :unregistered }
+
+ it { is_expected.to eq(:never_contacted) }
+
+ context 'if stale' do
+ let(:traits) { %i[unregistered stale] }
+
+ it { is_expected.to eq(:stale) }
+ end
context 'if created recently' do
- let(:runner_manager) { build(:ci_runner_machine, :unregistered, :created_within_stale_deadline) }
+ let(:traits) { %i[unregistered online] }
- it { is_expected.to eq(:never_contacted) }
+ it { is_expected.to eq(:offline) }
context "when cache contains 'finished' creation_state" do
before do
@@ -478,31 +486,31 @@ RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :mo
end
end
- it { is_expected.to eq(:offline) }
+ it { is_expected.to eq(:online) }
end
end
end
context 'if contacted just now' do
- let(:runner_manager) { build(:ci_runner_machine, :online) }
+ let(:traits) { :online }
it { is_expected.to eq(:online) }
end
context 'if almost offline' do
- let(:runner_manager) { build(:ci_runner_machine, :almost_offline) }
+ let(:traits) { :almost_offline }
it { is_expected.to eq(:online) }
end
context 'if contacted recently' do
- let(:runner_manager) { build(:ci_runner_machine, :offline) }
+ let(:traits) { :offline }
it { is_expected.to eq(:offline) }
end
- context 'if contacted long time ago' do
- let(:runner_manager) { build(:ci_runner_machine, :stale) }
+ context 'if stale' do
+ let(:traits) { :stale }
it { is_expected.to eq(:stale) }
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 504065c0ac5..46745282f02 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -986,50 +986,62 @@ RSpec.describe Ci::Runner, type: :model, factory_default: :keep, feature_categor
describe '#status', :freeze_time do
let(:runner) { build(:ci_runner, *Array.wrap(traits)) }
- subject { runner.status }
+ subject(:status) { runner.status }
- context 'stale, never contacted' do
- let(:traits) { %i[unregistered stale] }
+ context 'if unregistered' do
+ let(:traits) { :unregistered }
- it { is_expected.to eq(:stale) }
+ it { is_expected.to eq(:never_contacted) }
- context 'created recently, never contacted', :clean_gitlab_redis_cache do
+ context 'if created recently' do
let(:traits) { %i[unregistered online] }
- it { is_expected.to eq(:never_contacted) }
+ it { is_expected.to eq(:offline) }
+ end
- context "when cache contains 'finished' creation_state" do
- before do
- Gitlab::Redis::Cache.with do |redis|
- cache_key = runner.send(:cache_attribute_key)
- redis.set(cache_key, Gitlab::Json.dump(creation_state: :finished))
+ context 'if stale' do
+ let(:traits) { %i[unregistered stale] }
+
+ it { is_expected.to eq(:stale) }
+
+ context 'created recently, never contacted', :clean_gitlab_redis_cache do
+ let(:traits) { %i[unregistered online] }
+
+ it { is_expected.to eq(:offline) }
+
+ context "when cache contains 'finished' creation_state" do
+ before do
+ Gitlab::Redis::Cache.with do |redis|
+ cache_key = runner.send(:cache_attribute_key)
+ redis.set(cache_key, Gitlab::Json.dump(creation_state: :finished))
+ end
end
- end
- it { is_expected.to eq(:online) }
+ it { is_expected.to eq(:online) }
+ end
end
end
end
- context 'online, paused' do
+ context 'if online, paused' do
let(:traits) { %i[paused online] }
it { is_expected.to eq(:online) }
end
- context 'online' do
+ context 'if online' do
let(:traits) { :almost_offline }
it { is_expected.to eq(:online) }
end
- context 'offline' do
+ context 'if offline' do
let(:traits) { :offline }
it { is_expected.to eq(:offline) }
end
- context 'stale' do
+ context 'if stale' do
let(:traits) { :stale }
it { is_expected.to eq(:stale) }
@@ -1154,12 +1166,12 @@ RSpec.describe Ci::Runner, type: :model, factory_default: :keep, feature_categor
it 'still updates contacted at in redis cache and database' do
expect(runner).to be_invalid
- expect_redis_update(contacted_at: Time.current, creation_state: :finished)
+ expect_redis_update(contacted_at: Time.current)
expect { heartbeat }.to change { runner.reload.read_attribute(:contacted_at) }
end
it 'only updates contacted at in redis cache and database' do
- expect_redis_update(contacted_at: Time.current, creation_state: :finished)
+ expect_redis_update(contacted_at: Time.current)
expect { heartbeat }.to change { runner.reload.read_attribute(:contacted_at) }
end
end
@@ -1179,22 +1191,6 @@ RSpec.describe Ci::Runner, type: :model, factory_default: :keep, feature_categor
end
end
- describe '#clear_heartbeat', :freeze_time do
- let!(:runner) { create(:ci_runner) }
-
- it 'clears contacted at' do
- expect do
- runner.heartbeat
- end.to change { runner.reload.contacted_at }.from(nil).to(Time.current)
- .and change { runner.reload.uncached_contacted_at }.from(nil).to(Time.current)
-
- expect do
- runner.clear_heartbeat
- end.to change { runner.reload.contacted_at }.from(Time.current).to(nil)
- .and change { runner.reload.uncached_contacted_at }.from(Time.current).to(nil)
- end
- end
-
describe '#destroy' do
let(:runner) { create(:ci_runner) }
diff --git a/spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb b/spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb
index 07c62d0aa4c..8f1998ac7b9 100644
--- a/spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb
+++ b/spec/models/concerns/use_sql_function_for_primary_key_lookups_spec.rb
@@ -14,65 +14,59 @@ RSpec.describe UseSqlFunctionForPrimaryKeyLookups, feature_category: :groups_and
end
end
- context 'when the use_sql_functions_for_primary_key_lookups FF is on' do
- before do
- stub_feature_flags(use_sql_functions_for_primary_key_lookups: true)
- end
+ it 'loads the correct record' do
+ expect(model.find(user.id).id).to eq(user.id)
+ end
- it 'loads the correct record' do
- expect(model.find(user.id).id).to eq(user.id)
- end
-
- it 'uses the function-based finder query' do
- query = <<~SQL
+ it 'uses the function-based finder query' do
+ query = <<~SQL
SELECT "users".* FROM find_users_by_id(#{user.id})#{' '}
AS users WHERE ("users"."id" IS NOT NULL) LIMIT 1
+ SQL
+ query_log = ActiveRecord::QueryRecorder.new { model.find(user.id) }.log
+
+ expect(query_log).to match_array(include(query.tr("\n", '')))
+ end
+
+ it 'uses query cache', :use_sql_query_cache do
+ query = <<~SQL
+ SELECT "users".* FROM find_users_by_id(#{user.id})#{' '}
+ AS users WHERE ("users"."id" IS NOT NULL) LIMIT 1
+ SQL
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ model.find(user.id)
+ model.find(user.id)
+ model.find(user.id)
+ end
+
+ expect(recorder.data.each_value.first[:count]).to eq(1)
+ expect(recorder.cached).to include(query.tr("\n", ''))
+ end
+
+ context 'when the model has ignored columns' do
+ around do |example|
+ model.ignored_columns = %i[encrypted_password]
+ example.run
+ model.ignored_columns = []
+ end
+
+ it 'enumerates the column names' do
+ column_list = model.columns.map do |column|
+ %("users"."#{column.name}")
+ end.join(', ')
+
+ expect(column_list).not_to include(%("users"."encrypted_password"))
+
+ query = <<~SQL
+ SELECT #{column_list} FROM find_users_by_id(#{user.id})#{' '}
+ AS users WHERE ("users"."id" IS NOT NULL) LIMIT 1
SQL
query_log = ActiveRecord::QueryRecorder.new { model.find(user.id) }.log
expect(query_log).to match_array(include(query.tr("\n", '')))
end
- it 'uses query cache', :use_sql_query_cache do
- query = <<~SQL
- SELECT "users".* FROM find_users_by_id(#{user.id})#{' '}
- AS users WHERE ("users"."id" IS NOT NULL) LIMIT 1
- SQL
-
- recorder = ActiveRecord::QueryRecorder.new do
- model.find(user.id)
- model.find(user.id)
- model.find(user.id)
- end
-
- expect(recorder.data.each_value.first[:count]).to eq(1)
- expect(recorder.cached).to include(query.tr("\n", ''))
- end
-
- context 'when the model has ignored columns' do
- around do |example|
- model.ignored_columns = %i[encrypted_password]
- example.run
- model.ignored_columns = []
- end
-
- it 'enumerates the column names' do
- column_list = model.columns.map do |column|
- %("users"."#{column.name}")
- end.join(', ')
-
- expect(column_list).not_to include(%("users"."encrypted_password"))
-
- query = <<~SQL
- SELECT #{column_list} FROM find_users_by_id(#{user.id})#{' '}
- AS users WHERE ("users"."id" IS NOT NULL) LIMIT 1
- SQL
- query_log = ActiveRecord::QueryRecorder.new { model.find(user.id) }.log
-
- expect(query_log).to match_array(include(query.tr("\n", '')))
- end
- end
-
context 'when there are scope attributes' do
let(:scoped_model) do
Class.new(model) do
@@ -222,21 +216,4 @@ RSpec.describe UseSqlFunctionForPrimaryKeyLookups, feature_category: :groups_and
end
end
end
-
- context 'when the use_sql_functions_for_primary_key_lookups FF is off' do
- before do
- stub_feature_flags(use_sql_functions_for_primary_key_lookups: false)
- end
-
- it 'loads the correct record' do
- expect(model.find(user.id).id).to eq(user.id)
- end
-
- it 'uses the SQL-based finder query' do
- expected_query = %(SELECT "users".* FROM \"users\" WHERE "users"."id" = #{user.id} LIMIT 1)
- query_log = ActiveRecord::QueryRecorder.new { model.find(user.id) }.log
-
- expect(query_log).to match_array(include(expected_query))
- end
- end
end
diff --git a/spec/models/namespaces/preloaders/group_root_ancestor_preloader_spec.rb b/spec/models/namespaces/preloaders/group_root_ancestor_preloader_spec.rb
index b9462a4c21a..0be50ed072f 100644
--- a/spec/models/namespaces/preloaders/group_root_ancestor_preloader_spec.rb
+++ b/spec/models/namespaces/preloaders/group_root_ancestor_preloader_spec.rb
@@ -18,13 +18,7 @@ RSpec.describe Namespaces::Preloaders::GroupRootAncestorPreloader, feature_categ
create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer', parent: root_parent2)
end
- let(:root_query_regex) do
- if Feature.enabled?(:use_sql_functions_for_primary_key_lookups, Feature.current_request)
- /\ASELECT.+ FROM find_namespaces_by_id\(\d+\)/
- else
- /\ASELECT.+FROM "namespaces" WHERE "namespaces"."id" = \d+/
- end
- end
+ let(:root_query_regex) { /\ASELECT.+ FROM find_namespaces_by_id\(\d+\)/ }
let(:additional_preloads) { [] }
let(:groups) { [guest_group, private_maintainer_group, private_developer_group, public_maintainer_group] }
diff --git a/spec/models/namespaces/preloaders/namespace_root_ancestor_preloader_spec.rb b/spec/models/namespaces/preloaders/namespace_root_ancestor_preloader_spec.rb
index 1989656893a..be6254009f9 100644
--- a/spec/models/namespaces/preloaders/namespace_root_ancestor_preloader_spec.rb
+++ b/spec/models/namespaces/preloaders/namespace_root_ancestor_preloader_spec.rb
@@ -9,14 +9,7 @@ RSpec.describe Namespaces::Preloaders::NamespaceRootAncestorPreloader, feature_c
let_it_be(:public_group) { create(:group, :private, parent: parent_public_group) }
let_it_be(:private_group) { create(:group, :private, project_creation_level: nil) }
- let(:root_query_regex) do
- if Feature.enabled?(:use_sql_functions_for_primary_key_lookups, Feature.current_request)
- /\ASELECT.+ FROM find_namespaces_by_id\(\d+\)/
- else
- /\ASELECT.+FROM "namespaces" WHERE "namespaces"."id" = \d+/
- end
- end
-
+ let(:root_query_regex) { /\ASELECT.+ FROM find_namespaces_by_id\(\d+\)/ }
let(:additional_preloads) { [] }
let(:namespaces) { [project_namespace, public_group, private_group] }
let(:pristine_namespaces) { Namespace.where(id: namespaces) }
diff --git a/spec/models/namespaces/preloaders/project_root_ancestor_preloader_spec.rb b/spec/models/namespaces/preloaders/project_root_ancestor_preloader_spec.rb
index 3526b999dbe..38a76bf9189 100644
--- a/spec/models/namespaces/preloaders/project_root_ancestor_preloader_spec.rb
+++ b/spec/models/namespaces/preloaders/project_root_ancestor_preloader_spec.rb
@@ -18,13 +18,7 @@ RSpec.describe Namespaces::Preloaders::ProjectRootAncestorPreloader, feature_cat
create(:project, name: 'a public maintainer', path: 'a-public-maintainer', namespace: root_parent2)
end
- let(:root_query_regex) do
- if Feature.enabled?(:use_sql_functions_for_primary_key_lookups, Feature.current_request)
- /\ASELECT.+ FROM find_namespaces_by_id\(\d+\)/
- else
- /\ASELECT.+FROM "namespaces" WHERE "namespaces"."id" = \d+/
- end
- end
+ let(:root_query_regex) { /\ASELECT.+ FROM find_namespaces_by_id\(\d+\)/ }
let(:additional_preloads) { [] }
let(:projects) { [guest_project, private_maintainer_project, private_developer_project, public_maintainer_project] }
diff --git a/spec/requests/api/ci/runner/runners_verify_post_spec.rb b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
index 0f2489f0f2b..93f2e3e7801 100644
--- a/spec/requests/api/ci/runner/runners_verify_post_spec.rb
+++ b/spec/requests/api/ci/runner/runners_verify_post_spec.rb
@@ -16,8 +16,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
end
describe '/api/v4/runners' do
- describe 'POST /api/v4/runners/verify', :freeze_time do
- let_it_be_with_reload(:runner) { create(:ci_runner, token_expires_at: 3.days.from_now) }
+ describe 'POST /api/v4/runners/verify', :freeze_time, :clean_gitlab_redis_cache do
+ let_it_be_with_reload(:runner) { create(:ci_runner, :unregistered, token_expires_at: 3.days.from_now) }
let(:params) { nil }
@@ -50,9 +50,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
context 'with glrt-prefixed token' do
let_it_be(:registration_token) { 'glrt-abcdefg123456' }
- let_it_be(:registration_type) { :authenticated_user }
let_it_be(:runner) do
- create(:ci_runner, registration_type: registration_type,
+ create(:ci_runner, :unregistered, registration_type: :authenticated_user,
token: registration_token, token_expires_at: 3.days.from_now)
end
@@ -67,8 +66,8 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
})
end
- it 'does not update contacted_at' do
- expect { verify }.not_to change { runner.reload.contacted_at }.from(nil)
+ it 'does not update creation_state' do
+ expect { verify }.not_to change { runner.reload.creation_state }.from('started')
end
end
@@ -87,6 +86,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
expect { verify }.to change { runner.reload.contacted_at }.from(nil).to(Time.current)
end
+ it 'does not update creation_state' do
+ expect { verify }.not_to change { runner.reload.creation_state }.from('started')
+ end
+
context 'with non-expiring runner token' do
before do
runner.update!(token_expires_at: nil)
diff --git a/spec/requests/api/graphql/mutations/ci/runner/bulk_pause_spec.rb b/spec/requests/api/graphql/mutations/ci/runner/bulk_pause_spec.rb
new file mode 100644
index 00000000000..935b50d8fa3
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/ci/runner/bulk_pause_spec.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'RunnerBulkPause', feature_category: :runner do
+ include GraphqlHelpers
+
+ let_it_be(:admin) { create(:admin) }
+
+ let_it_be(:non_admin_user) { create(:user) }
+
+ let!(:runners_active) { create_list(:ci_runner, 2) }
+ let!(:runners_paused) { create_list(:ci_runner, 2, :paused) }
+ let!(:all_runners) { runners_paused + runners_active }
+
+ let(:mutation) do
+ graphql_mutation(
+ :runner_bulk_pause,
+ mutation_params,
+ <<-QL
+ updatedCount
+ updatedRunners {
+ id
+ paused
+ }
+ errors
+ QL
+ )
+ end
+
+ let(:mutation_response) { graphql_mutation_response(:runner_bulk_pause) }
+
+ context 'when user is admin' do
+ context 'when runners are active' do
+ let(:mutation_params) do
+ {
+ ids: runners_active.map(&:to_global_id)
+ }.deep_merge(mutation_scope_params)
+ end
+
+ context 'when asked to pause' do
+ let(:mutation_scope_params) do
+ {
+ paused: true
+ }
+ end
+
+ it 'pauses runners' do
+ post_graphql_mutation(mutation, current_user: admin)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['updatedCount']).to eq(2)
+ expect(mutation_response['updatedRunners']).to match_array(
+ runners_active.map { |runner| a_graphql_entity_for(runner, paused: true) }
+ )
+ end
+ end
+ end
+
+ context 'when runners are paused' do
+ let(:mutation_params) do
+ {
+ ids: runners_paused.map(&:to_global_id)
+ }.deep_merge(mutation_scope_params)
+ end
+
+ context 'when asked to unpause' do
+ let(:mutation_scope_params) do
+ {
+ paused: false
+ }
+ end
+
+ it 'unpauses runners' do
+ post_graphql_mutation(mutation, current_user: admin)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['updatedCount']).to eq(2)
+ expect(mutation_response['updatedRunners']).to match_array(
+ runners_paused.map { |runner| a_graphql_entity_for(runner, paused: false) }
+ )
+ end
+ end
+ end
+
+ context 'when runners have different active status' do
+ let(:mutation_params) do
+ {
+ ids: all_runners.map(&:to_global_id)
+ }.deep_merge(mutation_scope_params)
+ end
+
+ context 'when asked to unpause' do
+ let(:mutation_scope_params) do
+ {
+ paused: false
+ }
+ end
+
+ it 'unpauses every runner' do
+ post_graphql_mutation(mutation, current_user: admin)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['updatedCount']).to eq(4)
+ expect(mutation_response['updatedRunners']).to match_array(
+ all_runners.map { |runner| a_graphql_entity_for(runner, paused: false) }
+ )
+ end
+ end
+
+ context 'when asked to pause' do
+ let(:mutation_scope_params) do
+ {
+ paused: true
+ }
+ end
+
+ it 'pauses every runner' do
+ post_graphql_mutation(mutation, current_user: admin)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['updatedCount']).to eq(4)
+ expect(mutation_response['updatedRunners']).to match_array(
+ all_runners.map { |runner| a_graphql_entity_for(runner, paused: true) }
+ )
+ end
+ end
+ end
+
+ context 'with empty id list provided' do
+ let(:mutation_params) do
+ {
+ ids: [],
+ paused: true
+ }
+ end
+
+ it "doesn't fail" do
+ post_graphql_mutation(mutation, current_user: admin)
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['errors']).to be_empty
+ expect(mutation_response['updatedCount']).to eq(0)
+ end
+ end
+ end
+
+ context "when user doesn't have permission" do
+ let(:mutation_params) do
+ {
+ ids: runners_active.map(&:to_global_id),
+ paused: true
+ }
+ end
+
+ it 'returns an error' do
+ post_graphql_mutation(mutation, current_user: non_admin_user)
+ expect(mutation_response['errors'][0]).to include "User does not have permission to update / pause"
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index 1321d81e327..4126e8bbdab 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -613,6 +613,7 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
... on WorkItemWidgetAwardEmoji {
upvotes
downvotes
+ newCustomEmojiPath
awardEmoji {
nodes {
name
@@ -631,6 +632,7 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do
'type' => 'AWARD_EMOJI',
'upvotes' => work_item.upvotes,
'downvotes' => work_item.downvotes,
+ 'newCustomEmojiPath' => Gitlab::Routing.url_helpers.new_group_custom_emoji_path(group),
'awardEmoji' => {
'nodes' => match_array(
[emoji, upvote, downvote].map { |e| { 'name' => e.name } }
diff --git a/spec/services/ci/runners/bulk_pause_runners_service_spec.rb b/spec/services/ci/runners/bulk_pause_runners_service_spec.rb
new file mode 100644
index 00000000000..20895d26838
--- /dev/null
+++ b/spec/services/ci/runners/bulk_pause_runners_service_spec.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Ci::Runners::BulkPauseRunnersService, '#execute', feature_category: :fleet_visibility do
+ subject(:execute) { described_class.new(**service_args).execute }
+
+ let_it_be(:project1) { create(:project) }
+
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:maintainer) { create(:user, maintainer_of: project1) }
+ let_it_be(:user) { create(:user) }
+
+ let_it_be(:instance_runners_active) { create_list(:ci_runner, 2) }
+ let_it_be(:instance_runners_paused) { create_list(:ci_runner, 2, :paused) }
+ let_it_be(:project_runners_active) { create_list(:ci_runner, 2, :project, projects: [project1]) }
+
+ let(:service_args) do
+ {
+ runners: target_runners,
+ current_user: current_user,
+ paused: paused
+ }
+ end
+
+ context 'without target_runners' do
+ let(:target_runners) { nil }
+ let(:paused) { false }
+ let(:current_user) { admin }
+
+ it 'return 0 paused runners' do
+ expect(execute).to be_success
+ expect(execute.payload).to eq(updated_count: 0, updated_runners: [], errors: [])
+ end
+ end
+
+ context 'with admin right', :enable_admin_mode do
+ let(:current_user) { admin }
+
+ context 'when targeting instance runners' do
+ let(:target_runners) { Ci::Runner.instance_type }
+
+ context 'when activating paused runners' do
+ let(:target_runners) { Ci::Runner.instance_type.paused }
+ let(:paused) { false }
+
+ it 'unpauses runners' do
+ expect(execute).to be_success
+ expect(execute.payload[:updated_count]).to eq 2
+ expect(execute.payload[:errors]).to be_empty
+ expect(execute.payload[:updated_runners]).to all(be_active)
+ end
+ end
+
+ context 'when pausing active runners' do
+ let(:target_runners) { Ci::Runner.instance_type.active }
+ let(:paused) { true }
+
+ it 'pauses runners' do
+ expect(execute).to be_success
+ expect(execute.payload[:updated_count]).to eq 2
+ expect(execute.payload[:errors]).to be_empty
+ expect(execute.payload[:updated_runners]).not_to include(be_active)
+ end
+
+ context 'with too many runners specified' do
+ before do
+ stub_const("#{described_class}::RUNNER_LIMIT", 1)
+ end
+
+ it 'only pauses first runner' do
+ expect(execute).to be_success
+ expect(execute.payload[:updated_count]).to eq 1
+ expect(execute.payload[:updated_runners]).not_to include(be_active)
+ expect(target_runners.map(&:id)).to include(execute.payload[:updated_runners].first.id)
+ end
+ end
+ end
+
+ context 'when pausing mixed-state runners' do
+ let(:target_runners) { Ci::Runner.instance_type }
+ let(:paused) { true }
+
+ it 'pauses runners' do
+ expect(execute).to be_success
+ expect(execute.payload[:updated_count]).to eq 4
+ expect(execute.payload[:errors]).to be_empty
+ expect(execute.payload[:updated_runners]).not_to include(be_active)
+ end
+ end
+ end
+ end
+
+ context 'when user is maintainer' do
+ let(:current_user) { maintainer }
+
+ context 'when activating mixed-state instance runners' do
+ let(:target_runners) { Ci::Runner.instance_type }
+ let(:paused) { false }
+
+ it 'returns errors' do
+ expect(execute).to be_success
+ expect(execute.payload[:updated_count]).to eq 0
+ expect(execute.payload[:errors].first).to include "User does not have permission to update / pause"
+ expect(execute.payload[:updated_runners]).to be_empty
+ end
+ end
+
+ context 'when pausing active project runners' do
+ let(:target_runners) { Ci::Runner.project_type.active }
+ let(:paused) { true }
+
+ it 'pauses the runners' do
+ expect(execute).to be_success
+ expect(execute.payload[:updated_count]).to eq 2
+ expect(execute.payload[:errors]).to be_empty
+ expect(execute.payload[:updated_runners]).not_to include(be_active)
+ end
+ end
+ end
+
+ context 'when user is not member' do
+ let(:current_user) { user }
+
+ context 'when pausing active project runners' do
+ let(:target_runners) { Ci::Runner.project_type.active }
+ let(:paused) { true }
+
+ it 'returns errors' do
+ expect(execute).to be_success
+ expect(execute.payload[:updated_count]).to eq 0
+ expect(execute.payload[:errors].first).to include "User does not have permission to update / pause"
+ expect(execute.payload[:updated_runners]).to be_empty
+ end
+ end
+ end
+
+ context 'when user has permissions on only some runners' do
+ let(:current_user) { maintainer }
+
+ context 'when pausing active runners' do
+ let(:target_runners) { Ci::Runner.active }
+ let(:paused) { true }
+
+ it 'returns errors' do
+ expect(execute).to be_success
+ expect(execute.payload[:updated_count]).to eq 2
+ expect(execute.payload[:errors].first).to include "User does not have permission to update / pause runner(s)"
+ expect(execute.payload[:updated_runners]).not_to include(be_active)
+ expect(execute.payload[:updated_runners]).to match_array(project_runners_active)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/runners/unregister_runner_manager_service_spec.rb b/spec/services/ci/runners/unregister_runner_manager_service_spec.rb
index 367cff7863a..a1c31f14480 100644
--- a/spec/services/ci/runners/unregister_runner_manager_service_spec.rb
+++ b/spec/services/ci/runners/unregister_runner_manager_service_spec.rb
@@ -35,23 +35,13 @@ RSpec.describe ::Ci::Runners::UnregisterRunnerManagerService, '#execute', :freez
expect(runner.runner_managers).to contain_exactly(runner_manager2)
end
- it 'does not clear runner heartbeat' do
- expect(runner).not_to receive(:clear_heartbeat)
-
- expect(execute).to be_success
- end
-
context "when there are no runner managers left after deletion" do
let!(:runner_manager2) { nil }
- it 'clears the heartbeat attributes' do
- expect(runner).to receive(:clear_heartbeat).and_call_original
-
+ it 'does not clear the contacted_at value' do
expect do
expect(execute).to be_success
- end.to change { runner.reload.read_attribute(:contacted_at) }
- .from(a_kind_of(ActiveSupport::TimeWithZone))
- .to(nil)
+ end.not_to change { runner.reload.read_attribute(:contacted_at) }
end
end
end
diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index d4bf82f536a..945aac2fd4c 100644
--- a/spec/support/helpers/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
@@ -2,7 +2,7 @@
module SearchHelpers
def fill_in_search(text)
- click_button "Search or go to…"
+ find_by_testid('super-sidebar-search-button').click
fill_in 'search', with: text
wait_for_all_requests
diff --git a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb
index bfcf43daea9..3bc3a4cb2d6 100644
--- a/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb
+++ b/spec/support/shared_examples/config/metrics/every_metric_definition_shared_examples.rb
@@ -8,13 +8,6 @@ RSpec.shared_examples 'every metric definition' do
%w[
testing_total_unique_counts
user_auth_by_provider
- counts.groups_google_cloud_platform_artifact_registry_active
- counts.groups_inheriting_google_cloud_platform_artifact_registry_active
- counts.groups_inheriting_google_cloud_platform_workload_identity_federation_active
- counts.instances_google_cloud_platform_artifact_registry_active
- counts.instances_google_cloud_platform_workload_identity_federation_active
- counts.projects_inheriting_google_cloud_platform_artifact_registry_active
- counts.projects_inheriting_google_cloud_platform_workload_identity_federation_active
].freeze
end
diff --git a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
index f1a97e05ea8..c8be88c3fa6 100644
--- a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
+++ b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
@@ -16,24 +16,26 @@
# - property
# - value
# [Recommended] Prefer including these attributes via additional_properties instead.
-# ex) let(:additional_properties) { { label: "value" } }
+# ex) let(:additional_properties) { { label: "label_name" } }
RSpec.shared_examples 'internal event tracking' do
let(:all_metrics) do
- additional_properties = try(:additional_properties) || {}
- base_additional_properties = Gitlab::Tracking::EventValidator::BASE_ADDITIONAL_PROPERTIES.to_h do |key, _val|
- [key, try(key)]
- end
-
Gitlab::Usage::MetricDefinition.all.filter_map do |definition|
- matching_rules = definition.event_selection_rules.map do |event_selection_rule|
+ matching_rules = definition.event_selection_rules.select do |event_selection_rule|
next unless event_selection_rule.name == event
# Only include unique metrics if the unique_identifier_name is present in the spec
next if event_selection_rule.unique_identifier_name && !try(event_selection_rule.unique_identifier_name)
- properties = additional_properties.merge(base_additional_properties)
- event_selection_rule.matches?(properties)
+ next true if event_selection_rule.filter.blank?
+
+ raise <<~MESSAGE
+ Event '#{event}' has metrics that use filters.
+ Testing such events with the 'internal event tracking' examples group is not supported.
+
+ To test it, use composable matchers:
+ https://docs.gitlab.com/development/internal_analytics/internal_event_instrumentation/quick_start/#composable-matchers
+ MESSAGE
end
definition.key if matching_rules.flatten.any?
diff --git a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
index 8540b1961f0..37272c25e90 100644
--- a/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/create_service_shared_examples.rb
@@ -49,8 +49,13 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
subject(:track_event) { service.execute }
- it_behaves_like 'internal event tracking' do
- let(:event) { 'create_wiki_page' }
+ it "triggers an internal event" do
+ expect { track_event }.to trigger_internal_events('create_wiki_page').with(
+ category: 'InternalEventTracking',
+ user: user,
+ project: project,
+ namespace: namespace
+ )
end
context 'with group container', if: container_type == :group do
@@ -64,19 +69,19 @@ RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
let(:event) { 'create_group_wiki_page' }
end
end
- end
- context 'when the new page is a template' do
- let(:page_title) { "#{Wiki::TEMPLATES_DIR}/foobar" }
+ context 'when the new page is a template' do
+ let(:page_title) { "#{Wiki::TEMPLATES_DIR}/foobar" }
- it_behaves_like 'internal event tracking' do
- let(:event) { 'create_wiki_page' }
- let(:project) { container if container.is_a?(Project) }
- let(:namespace) { container.is_a?(Group) ? container : container.namespace }
- let(:label) { 'template' }
- let(:property) { 'markdown' }
-
- subject(:track_event) { service.execute }
+ it "triggers an internal event" do
+ expect { track_event }.to trigger_internal_events('create_wiki_page').with(
+ category: 'InternalEventTracking',
+ user: user,
+ project: project,
+ namespace: namespace,
+ additional_properties: { label: 'template', property: 'markdown' }
+ )
+ end
end
end
diff --git a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
index c2cdfdf15f6..4c3ee56a3ef 100644
--- a/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/destroy_service_shared_examples.rb
@@ -21,8 +21,13 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
subject(:track_event) { service.execute(page) }
- it_behaves_like 'internal event tracking' do
- let(:event) { 'delete_wiki_page' }
+ it "triggers an internal event" do
+ expect { track_event }.to trigger_internal_events('delete_wiki_page').with(
+ category: 'InternalEventTracking',
+ user: user,
+ project: project,
+ namespace: namespace
+ )
end
context 'with group container', if: container_type == :group do
@@ -40,10 +45,14 @@ RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
context 'when the deleted page is a template' do
let(:page) { create(:wiki_page, title: "#{Wiki::TEMPLATES_DIR}/foobar") }
- it_behaves_like 'internal event tracking' do
- let(:event) { 'delete_wiki_page' }
- let(:label) { 'template' }
- let(:property) { 'markdown' }
+ it "triggers an internal event" do
+ expect { track_event }.to trigger_internal_events('delete_wiki_page').with(
+ category: 'InternalEventTracking',
+ user: user,
+ project: project,
+ namespace: namespace,
+ additional_properties: { label: 'template', property: 'markdown' }
+ )
end
end
end
diff --git a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
index 0a055e66216..5b3818d32e2 100644
--- a/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/wiki_pages/update_service_shared_examples.rb
@@ -64,8 +64,13 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
subject(:track_event) { service.execute(page) }
- it_behaves_like 'internal event tracking' do
- let(:event) { 'update_wiki_page' }
+ it "triggers an internal event" do
+ expect { track_event }.to trigger_internal_events('update_wiki_page').with(
+ category: 'InternalEventTracking',
+ user: user,
+ project: project,
+ namespace: namespace
+ )
end
context 'with group container', if: container_type == :group do
@@ -79,19 +84,19 @@ RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
let(:event) { 'update_group_wiki_page' }
end
end
- end
- context 'when the updated page is a template' do
- let(:page) { create(:wiki_page, title: "#{Wiki::TEMPLATES_DIR}/foobar") }
+ context 'when the updated page is a template' do
+ let(:page) { create(:wiki_page, title: "#{Wiki::TEMPLATES_DIR}/foobar") }
- it_behaves_like 'internal event tracking' do
- let(:event) { 'update_wiki_page' }
- let(:project) { container if container.is_a?(Project) }
- let(:namespace) { container.is_a?(Group) ? container : container.namespace }
- let(:label) { 'template' }
- let(:property) { 'markdown' }
-
- subject(:track_event) { service.execute(page) }
+ it "triggers an internal event" do
+ expect { track_event }.to trigger_internal_events('update_wiki_page').with(
+ category: 'InternalEventTracking',
+ user: user,
+ project: project,
+ namespace: namespace,
+ additional_properties: { label: 'template', property: 'markdown' }
+ )
+ end
end
end
diff --git a/spec/support_specs/matchers/internal_events_matchers_spec.rb b/spec/support_specs/matchers/internal_events_matchers_spec.rb
index a32b4008c1e..357d95f53f2 100644
--- a/spec/support_specs/matchers/internal_events_matchers_spec.rb
+++ b/spec/support_specs/matchers/internal_events_matchers_spec.rb
@@ -490,7 +490,7 @@ RSpec.describe 'Internal Events matchers', :clean_gitlab_redis_shared_state, fea
end
context 'with additional properties' do
- let(:event) { 'push_package_to_registry' }
+ let(:event) { 'g_edit_by_sfe' }
let(:user) { user_1 }
let(:project) { project_1 }
let(:expected_label) { 'Awesome label value' }
@@ -537,6 +537,19 @@ RSpec.describe 'Internal Events matchers', :clean_gitlab_redis_shared_state, fea
end
end
end
+
+ context "with an event that has metrics using filters" do
+ let(:event) { 'push_package_to_registry' }
+
+ subject(:assertion) { track_event }
+
+ before do
+ # Mark examples as pending so that a passing test will raise an error.
+ pending('This example should always fail. Protects against false positives.')
+ end
+
+ it_behaves_like 'internal event tracking'
+ end
end
private
diff --git a/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue b/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue
index 36985bb0125..7c8fc724062 100644
--- a/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue
+++ b/vendor/assets/javascripts/vue-virtual-scroller/src/components/RecycleScroller.vue
@@ -619,7 +619,11 @@ export default {
: { scroll: 'scrollLeft', start: 'left' }
if (this.pageMode) {
- const viewportEl = ScrollParent(this.$el)
+ let viewportEl = ScrollParent(this.$el)
+ if (viewportEl.tagName === 'BODY') {
+ // is always a scroll container even when has overflow: scroll
+ viewportEl = document.documentElement
+ }
// HTML doesn't overflow like other elements
const scrollTop = viewportEl.tagName === 'HTML' ? 0 : viewportEl[direction.scroll]
const viewport = viewportEl.getBoundingClientRect()
diff --git a/yarn.lock b/yarn.lock
index 8a96d4421a2..af71f979823 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1374,10 +1374,10 @@
core-js "^3.29.1"
mitt "^3.0.1"
-"@gitlab/duo-ui@^8.9.1":
- version "8.9.1"
- resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-8.9.1.tgz#06508884ab09cfcd7fb828b1f3b1256d5a9a4cbb"
- integrity sha512-/R+2jDeSg+x2ijAzp4NGVbuy4t9yfC0EKAmB3DE5JAQBDaKYaeFBfJo7Z+UCCtGAY+jGa2coMwpnIJaTiEPlVA==
+"@gitlab/duo-ui@^8.10.0":
+ version "8.10.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-8.10.0.tgz#2a7d2080045c4becc8df7564314010615418bef8"
+ integrity sha512-mpODFHF6xPnXbY7/uC9CWCZl7BfMNAAd4qu2ok6VCDsaT2TlkP50IbQ6WjUBuIBHwhofmyDsZ02Xi5bESv52eQ==
dependencies:
"@floating-ui/dom" "1.4.3"
echarts "^5.3.2"
@@ -10423,22 +10423,22 @@ markdownlint-cli2-formatter-default@0.0.5:
resolved "https://registry.yarnpkg.com/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.5.tgz#b8fde4e127f9a9c0596e6d45eed352dd0aa0ff98"
integrity sha512-4XKTwQ5m1+Txo2kuQ3Jgpo/KmnG+X90dWt4acufg6HVGadTUG5hzHF/wssp9b5MBYOMCnZ9RMPaU//uHsszF8Q==
-markdownlint-cli2@^0.17.1:
- version "0.17.1"
- resolved "https://registry.yarnpkg.com/markdownlint-cli2/-/markdownlint-cli2-0.17.1.tgz#4455105fe5d75821597779f7a442ce5c5ce0fede"
- integrity sha512-n1Im9lhKJJE12/u2N0GWBwPqeb0HGdylN8XpSFg9hbj35+QalY9Vi6mxwUQdG6wlSrrIq9ZDQ0Q85AQG9V2WOg==
+markdownlint-cli2@^0.17.2:
+ version "0.17.2"
+ resolved "https://registry.yarnpkg.com/markdownlint-cli2/-/markdownlint-cli2-0.17.2.tgz#8d3dc2637fae42cea01fe3bf218501cbf33a7cd5"
+ integrity sha512-XH06ZOi8wCrtOSSj3p8y3yJzwgzYOSa7lglNyS3fP05JPRzRGyjauBb5UvlLUSCGysMmULS1moxdRHHudV+g/Q==
dependencies:
globby "14.0.2"
js-yaml "4.1.0"
jsonc-parser "3.3.1"
- markdownlint "0.37.3"
+ markdownlint "0.37.4"
markdownlint-cli2-formatter-default "0.0.5"
micromatch "4.0.8"
-markdownlint@0.37.3:
- version "0.37.3"
- resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.37.3.tgz#061ac5462e97fedc7a96aaac5c132885e4161bfd"
- integrity sha512-eoQqH0291YCCjd+Pe1PUQ9AmWthlVmS0XWgcionkZ8q34ceZyRI+pYvsWksXJJL8OBkWCPwp1h/pnXxrPFC4oA==
+markdownlint@0.37.4:
+ version "0.37.4"
+ resolved "https://registry.yarnpkg.com/markdownlint/-/markdownlint-0.37.4.tgz#dd58c4a13b798d4702438e5f7fd587a219f753f6"
+ integrity sha512-u00joA/syf3VhWh6/ybVFkib5Zpj2e5KB/cfCei8fkSRuums6nyisTWGqjTWIOFoFwuXoTBQQiqlB4qFKp8ncQ==
dependencies:
markdown-it "14.1.0"
micromark "4.0.1"
@@ -15372,10 +15372,10 @@ vite-plugin-ruby@^5.1.1:
debug "^4.3.4"
fast-glob "^3.3.2"
-vite@^6.2.5:
- version "6.2.5"
- resolved "https://registry.yarnpkg.com/vite/-/vite-6.2.5.tgz#d093b5fe8eb96e594761584a966ab13f24457820"
- integrity sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==
+vite@^6.2.6:
+ version "6.2.6"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-6.2.6.tgz#7f0ccf2fdc0c1eda079ce258508728e2473d3f61"
+ integrity sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==
dependencies:
esbuild "^0.25.0"
postcss "^8.5.3"