diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 53284b582b8..8487494d589 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -1006,7 +1006,7 @@ lib/gitlab/checks/ /doc/integration/diffblue_cover.md @lyspin /doc/integration/elasticsearch/troubleshooting/ @ashrafkhamis /doc/integration/exact_code_search/ @ashrafkhamis -/doc/integration/external-issue-tracker.md @ashrafkhamis +/doc/integration/external-issue-tracker.md @msedlakjakubowski /doc/integration/gitpod.md @brendan777 /doc/integration/gmail_action_buttons_for_gitlab.md @ashrafkhamis /doc/integration/jenkins.md @lyspin @@ -1014,7 +1014,7 @@ lib/gitlab/checks/ /doc/integration/mattermost/ @axil @eread /doc/integration/snowflake.md @eread /doc/integration/sourcegraph.md @brendan777 -/doc/integration/trello_power_up.md @ashrafkhamis +/doc/integration/trello_power_up.md @msedlakjakubowski /doc/operations/ @lciutacu /doc/policy/ @axil @eread /doc/security/ @idurham @@ -1150,14 +1150,23 @@ lib/gitlab/checks/ /doc/user/project/import/jira_migration_options.md @msedlakjakubowski /doc/user/project/insights/ @lciutacu /doc/user/project/integrations/ @ashrafkhamis +/doc/user/project/integrations/asana.md @msedlakjakubowski /doc/user/project/integrations/bamboo.md @lyspin /doc/user/project/integrations/beyond_identity.md @brendan777 +/doc/user/project/integrations/bugzilla.md @msedlakjakubowski +/doc/user/project/integrations/clickup.md @msedlakjakubowski /doc/user/project/integrations/confluence.md @msedlakjakubowski +/doc/user/project/integrations/custom_issue_tracker.md @msedlakjakubowski +/doc/user/project/integrations/ewm.md @msedlakjakubowski /doc/user/project/integrations/git_guardian.md @brendan777 /doc/user/project/integrations/github.md @lyspin /doc/user/project/integrations/google_artifact_management.md @z_painter /doc/user/project/integrations/harbor.md @z_painter /doc/user/project/integrations/matrix.md @sselhorn +/doc/user/project/integrations/phorge.md @msedlakjakubowski +/doc/user/project/integrations/pivotal_tracker.md @msedlakjakubowski +/doc/user/project/integrations/redmine.md @msedlakjakubowski +/doc/user/project/integrations/youtrack.md @msedlakjakubowski /doc/user/project/issue_board.md @msedlakjakubowski /doc/user/project/issues/ @msedlakjakubowski /doc/user/project/issues/csv_import.md @ashrafkhamis diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml index 494e82d411f..74adaeefc46 100644 --- a/.gitlab/ci/qa-common/main.gitlab-ci.yml +++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml @@ -7,7 +7,7 @@ workflow: include: - local: .gitlab/ci/version.yml - local: .gitlab/ci/global.gitlab-ci.yml - - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@11.4.0" + - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@11.6.1" inputs: job_name: "e2e-test-report" job_stage: "report" @@ -17,7 +17,7 @@ include: gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE" allure_job_name: "${QA_RUN_TYPE}" - project: gitlab-org/quality/pipeline-common - ref: 11.4.0 + ref: 11.6.1 file: - /ci/notify-slack.gitlab-ci.yml - /ci/qa-report.gitlab-ci.yml diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 3604c335d06..69fa5f02608 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -86a2c426295d105b55455cd694e6929e4bce1d98 +6acf82daeb93a1c1eb21aad5d40c154675cd050f diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index 6b371f3cb5c..6fe7b08f497 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -489987d4c04b2eb18639b15d96795a6c6078b44f +3d9b3113d69fbe1c51b3a2b8eb95b08601798801 diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js index e1bd1012b35..5e3f142543f 100644 --- a/app/assets/javascripts/access_tokens/index.js +++ b/app/assets/javascripts/access_tokens/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import VueRouter from 'vue-router'; import { pinia } from '~/pinia/instance'; import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; @@ -14,6 +15,7 @@ import TokensApp from './components/tokens_app.vue'; import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from './constants'; Vue.use(Translate); +Vue.use(VueRouter); export const initAccessTokenTableApp = () => { const el = document.querySelector('#js-access-token-table-app'); @@ -148,9 +150,12 @@ export const initSharedAccessTokenApp = () => { accessTokenShow, } = el.dataset; + const router = new VueRouter({ mode: 'history' }); + return new Vue({ el, name: 'AccessTokensRoot', + router, pinia, provide: { accessTokenAvailableScopes: JSON.parse(accessTokenAvailableScopes), diff --git a/app/assets/javascripts/glql/components/presenters/list.vue b/app/assets/javascripts/glql/components/presenters/list.vue index afeb3d3ad6e..508dbd92890 100644 --- a/app/assets/javascripts/glql/components/presenters/list.vue +++ b/app/assets/javascripts/glql/components/presenters/list.vue @@ -81,7 +81,7 @@ export default { :count="items.length" persist-collapsed-state is-collapsible - class="!gl-mt-5" + class="!gl-my-5" @collapsed="isCollapsed = true" @expanded="isCollapsed = false" > diff --git a/app/assets/javascripts/glql/components/presenters/table.vue b/app/assets/javascripts/glql/components/presenters/table.vue index d1c25739569..8b133179358 100644 --- a/app/assets/javascripts/glql/components/presenters/table.vue +++ b/app/assets/javascripts/glql/components/presenters/table.vue @@ -71,7 +71,7 @@ export default { :count="items.length" is-collapsible persist-collapsed-state - class="!gl-mt-5" + class="!gl-my-5" :body-class="{ '!gl-m-0 !gl-p-0': items.length || isPreview }" @collapsed="isCollapsed = true" @expanded="isCollapsed = false" diff --git a/app/assets/javascripts/glql/core/index.js b/app/assets/javascripts/glql/core/index.js index df92df52874..bb514cd40e9 100644 --- a/app/assets/javascripts/glql/core/index.js +++ b/app/assets/javascripts/glql/core/index.js @@ -1,18 +1,18 @@ import { execute } from './executor'; import { parse } from './parser'; import { present } from './presenter'; -import { transform } from './transformer/data'; +import { transform } from './transformer'; export const executeAndPresentQuery = async (glqlQuery, queryKey) => { const { query, config, variables } = await parse(glqlQuery); const data = await execute(query, variables); - const transformed = transform(data, config); + const transformed = await transform(data, config); return present(transformed, config, { queryKey }); }; export const presentPreview = async (glqlQuery, queryKey) => { const { config } = await parse(glqlQuery); const data = { project: { issues: { nodes: [] } } }; - const transformed = transform(data, config); + const transformed = await transform(data, config); return present(transformed, config, { isPreview: true, queryKey }); }; diff --git a/app/assets/javascripts/glql/core/parser.js b/app/assets/javascripts/glql/core/parser.js new file mode 100644 index 00000000000..8ebf6422a5a --- /dev/null +++ b/app/assets/javascripts/glql/core/parser.js @@ -0,0 +1,56 @@ +import jsYaml from 'js-yaml'; +import { glql } from '@gitlab/query-language-rust'; +import { extractGroupOrProject } from '../utils/common'; +import { glqlFeatureFlags } from '../utils/feature_flags'; + +const DEFAULT_DISPLAY_FIELDS = 'title'; +const REQUIRED_QUERY_FIELDS = 'id, iid, title, webUrl, reference, state'; + +const isValidYAML = (text) => typeof jsYaml.safeLoad(text) === 'object'; + +export const parseYAMLConfig = (frontmatter) => { + const config = jsYaml.safeLoad(frontmatter) || {}; + + config.display = config.display || 'list'; + config.fields = config.fields || DEFAULT_DISPLAY_FIELDS; + + return config; +}; + +export const parseQueryTextWithFrontmatter = (text) => { + const frontmatter = text.match(/---\n([\s\S]*?)\n---/); + const remaining = text.replace(frontmatter ? frontmatter[0] : '', ''); + return { + frontmatter: frontmatter ? frontmatter[1].trim() : '', + query: remaining.trim(), + }; +}; + +export const parseQuery = async (query, config) => { + const { output, success, variables } = await glql.compile(query, { + ...config, + ...extractGroupOrProject(), + username: gon.current_username, + fields: `${REQUIRED_QUERY_FIELDS}, ${config.fields}`, + featureFlags: glqlFeatureFlags(), + }); + + if (!success) throw new Error(output); + + return { query: output, variables }; +}; + +export const parse = async (glqlQuery) => { + let { frontmatter: config, query } = parseQueryTextWithFrontmatter(glqlQuery); + if (!config && isValidYAML(glqlQuery)) { + // if frontmatter isn't present, query is a part of the config + ({ query, ...config } = parseYAMLConfig(glqlQuery)); + } else { + config = parseYAMLConfig(config); + } + + const limit = parseInt(config.limit, 10) || undefined; + const parsed = await parseQuery(query, { ...config, limit }); + + return { query: parsed.query, variables: parsed.variables, config }; +}; diff --git a/app/assets/javascripts/glql/core/parser/ast.js b/app/assets/javascripts/glql/core/parser/ast.js deleted file mode 100644 index 1fcf739be5f..00000000000 --- a/app/assets/javascripts/glql/core/parser/ast.js +++ /dev/null @@ -1,29 +0,0 @@ -import { __, sprintf } from '~/locale'; - -export const Types = { - FIELD_NAME: 'field_name', - STRING: 'string', - COLLECTION: 'collection', - FUNCTION_CALL: 'function_call', -}; - -class AstNode { - constructor(type, { ...data }) { - this.type = type; - Object.assign(this, data); - } - - withAlias(alias) { - if (![Types.FIELD_NAME, Types.FUNCTION_CALL].includes(this.type)) - throw new Error(sprintf(__('Unsupported node type for alias: %{type}'), { type: this.type })); - if (alias.type !== Types.STRING) throw new Error(__('Field alias must be of type `String`.')); - - this.alias = alias; - return this; - } -} - -export const fieldName = (name) => new AstNode(Types.FIELD_NAME, { value: name }); -export const string = (value) => new AstNode(Types.STRING, { value }); -export const functionCall = (name, args) => new AstNode(Types.FUNCTION_CALL, { name, args }); -export const collection = (...values) => new AstNode(Types.COLLECTION, { value: values }); diff --git a/app/assets/javascripts/glql/core/parser/combinators.js b/app/assets/javascripts/glql/core/parser/combinators.js deleted file mode 100644 index a3c8d311bb0..00000000000 --- a/app/assets/javascripts/glql/core/parser/combinators.js +++ /dev/null @@ -1,89 +0,0 @@ -import { __ } from '~/locale'; -import { truncate } from '~/lib/utils/text_utility'; - -const success = (value, rest) => ({ success: true, value, rest }); -const error = (expected, got) => ({ success: false, expected, got }); - -export const Parser = (parserFn) => ({ - run: parserFn, - map(fn) { - return Parser((input) => { - const result = this.run(input); - return result.success ? { ...result, value: fn(result.value) } : result; - }); - }, - chain(fn) { - return Parser((input) => { - const result = this.run(input); - return result.success ? fn(result.value).run(result.rest) : result; - }); - }, -}); - -export const tag = (s) => - Parser((input) => - input.startsWith(s) ? success(s, input.slice(s.length)) : error(s, input.slice(0, s.length)), - ); - -export const tagNoCase = (s) => - Parser((input) => - input.slice(0, s.length).toLowerCase() === s.toLowerCase() - ? success(input.slice(0, s.length), input.slice(s.length)) - : error(s, truncate(input, 10)), - ); - -export const regex = (re, description) => - Parser((input) => { - const match = input.match(re); - return match && match.index === 0 - ? success(match[0], input.slice(match[0].length)) - : error(description, truncate(input, 10)); - }); - -export const seq = (...parsers) => - Parser((input) => { - const results = []; - let currentInput = input; - for (const parser of parsers) { - const result = parser.run(currentInput); - if (!result.success) return result; - results.push(result.value); - currentInput = result.rest; - } - return success(results, currentInput); - }); - -export const alt = (...parsers) => - Parser((input) => { - for (const parser of parsers) { - const result = parser.run(input); - if (result.success) return result; - } - return error(__('something to parse'), truncate(input, 10)); - }); - -export const many = (parser) => - Parser((input) => { - const results = []; - let currentInput = input; - let result; - do { - result = parser.run(currentInput); - if (result.success) { - results.push(result.value); - currentInput = result.rest; - } - } while (result.success); - - return success(results, currentInput); - }); - -export const optional = (parser) => - alt( - parser, - Parser((input) => success(null, input)), - ); - -export const whitespace = regex(/^\s+/, 'whitespace'); - -export const token = (parser) => seq(optional(whitespace), parser).map(([, t]) => t); diff --git a/app/assets/javascripts/glql/core/parser/config.js b/app/assets/javascripts/glql/core/parser/config.js deleted file mode 100644 index fc345238770..00000000000 --- a/app/assets/javascripts/glql/core/parser/config.js +++ /dev/null @@ -1,14 +0,0 @@ -import jsYaml from 'js-yaml'; -import { transformAstToDisplayFields } from '../transformer/ast'; -import { parseFields } from './fields'; - -export const parseYAMLConfig = (frontmatter, defaults = {}) => { - const config = jsYaml.safeLoad(frontmatter) || {}; - - config.display = config.display || 'list'; - config.fields = transformAstToDisplayFields( - parseFields(config.fields || defaults?.fields.join(',')), - ); - - return config; -}; diff --git a/app/assets/javascripts/glql/core/parser/fields.js b/app/assets/javascripts/glql/core/parser/fields.js deleted file mode 100644 index 2cd1bfdb630..00000000000 --- a/app/assets/javascripts/glql/core/parser/fields.js +++ /dev/null @@ -1,48 +0,0 @@ -import { sprintf, __ } from '~/locale'; -import { truncate } from '~/lib/utils/text_utility'; -import { alt, many, optional, regex, seq, tag, tagNoCase, token } from './combinators'; -import * as ast from './ast'; - -const fieldName = token(regex(/^[a-z_][a-z0-9_]*/i, __('field name'))).map((name) => - ast.fieldName(name), -); - -const string = token(regex(/^"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/, __('string'))).map((s) => - ast.string(s.slice(1, -1)), -); - -const sepBy = (parser, separator) => - seq(parser, many(seq(separator, parser))).map(([first, rest]) => - ast.collection(first, ...rest.map(([, item]) => item)), - ); - -const leftParen = token(tag('(')); -const rightParen = token(tag(')')); -const comma = token(tag(',')); -const as = token(tagNoCase('as')); - -const functionName = token(regex(/^[a-z_][a-z0-9_]*/i, __('function name'))); -const functionArgs = optional(sepBy(string, comma)); -const functionCall = seq(functionName, leftParen, functionArgs, rightParen).map(([name, , args]) => - ast.functionCall(name, args), -); - -const value = alt(functionCall, fieldName); -const valueWithAlias = seq(value, as, string).map(([v, , alias]) => v.withAlias(alias)); -const parser = sepBy(alt(valueWithAlias, value), comma); - -// Parser function -export const parseFields = (input) => { - const result = parser.run(input); - const rest = result.rest.trim(); - if (rest) - throw new Error( - sprintf(__('Parse error: Unexpected input near `%{input}`.'), { - input: truncate(rest, 10), - }), - ); - - if (result.success) return result.value; - - throw new Error(sprintf(__('Parse error: Expected `%{expected}`, but got `%{got}`.'), result)); -}; diff --git a/app/assets/javascripts/glql/core/parser/index.js b/app/assets/javascripts/glql/core/parser/index.js deleted file mode 100644 index c99926de101..00000000000 --- a/app/assets/javascripts/glql/core/parser/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import jsYaml from 'js-yaml'; -import { parseYAMLConfig } from './config'; -import { parseQuery } from './query'; - -const DEFAULT_DISPLAY_FIELDS = ['title']; - -export const parseQueryTextWithFrontmatter = (text) => { - const frontmatter = text.match(/---\n([\s\S]*?)\n---/); - const remaining = text.replace(frontmatter ? frontmatter[0] : '', ''); - return { - frontmatter: frontmatter ? frontmatter[1].trim() : '', - query: remaining.trim(), - }; -}; - -const isValidYAML = (text) => typeof jsYaml.safeLoad(text) === 'object'; - -export const parse = async (glqlQuery, target = 'graphql') => { - let { frontmatter: config, query } = parseQueryTextWithFrontmatter(glqlQuery); - if (!config && isValidYAML(glqlQuery)) { - // if frontmatter isn't present, query is a part of the config - ({ query, ...config } = parseYAMLConfig(glqlQuery, { fields: DEFAULT_DISPLAY_FIELDS })); - } else { - config = parseYAMLConfig(config, { fields: DEFAULT_DISPLAY_FIELDS }); - } - - const limit = parseInt(config.limit, 10) || undefined; - const parsed = await parseQuery(query, { ...config, target, limit }); - - return { query: parsed.query, variables: parsed.variables, config }; -}; diff --git a/app/assets/javascripts/glql/core/parser/query.js b/app/assets/javascripts/glql/core/parser/query.js deleted file mode 100644 index 04b8c01dbe5..00000000000 --- a/app/assets/javascripts/glql/core/parser/query.js +++ /dev/null @@ -1,30 +0,0 @@ -import { uniq, once } from 'lodash'; -import { GitLabQueryLanguage as QueryParser } from '@gitlab/query-language-rust'; -import { extractGroupOrProject } from '../../utils/common'; -import { glqlWorkItemsFeatureFlagEnabled } from '../../utils/feature_flags'; - -const REQUIRED_QUERY_FIELDS = ['id', 'iid', 'title', 'webUrl', 'reference', 'state']; - -const initParser = once(async () => { - const parser = QueryParser(); - const { group, project } = extractGroupOrProject(); - - parser.group = group || ''; - parser.project = project || ''; - parser.username = gon.current_username || ''; - await parser.initialize(); - - return parser; -}); - -export const parseQuery = async (query, config) => { - const parser = await initParser(); - parser.fields = uniq([...REQUIRED_QUERY_FIELDS, ...config.fields.map(({ name }) => name)]); - - const target = glqlWorkItemsFeatureFlagEnabled() ? 'work_items_graphql' : 'graphql'; - const { output, variables } = parser.compile(target, query, config); - - if (output.toLowerCase().startsWith('error')) throw new Error(output.replace(/^error: /i, '')); - - return { query: output, variables }; -}; diff --git a/app/assets/javascripts/glql/core/transformer.js b/app/assets/javascripts/glql/core/transformer.js new file mode 100644 index 00000000000..9c27ea37930 --- /dev/null +++ b/app/assets/javascripts/glql/core/transformer.js @@ -0,0 +1,17 @@ +import { glql } from '@gitlab/query-language-rust'; +import { glqlFeatureFlags } from '../utils/feature_flags'; + +export const transform = async (data, config) => { + const result = await glql.transform(data, { + fields: config.fields, + featureFlags: glqlFeatureFlags(), + }); + + if (!result.success) throw new Error(result.error); + + // eslint-disable-next-line no-param-reassign + config.source = result.source || 'issues'; + // eslint-disable-next-line no-param-reassign + config.fields = result.fields; + return result.data; +}; diff --git a/app/assets/javascripts/glql/core/transformer/ast.js b/app/assets/javascripts/glql/core/transformer/ast.js deleted file mode 100644 index 0f3e1bb3e40..00000000000 --- a/app/assets/javascripts/glql/core/transformer/ast.js +++ /dev/null @@ -1,46 +0,0 @@ -import { uniqueId } from 'lodash'; -import { __, sprintf } from '~/locale'; -import { toSentenceCase } from '../../utils/common'; -import * as ast from '../parser/ast'; -import { getFieldAlias } from './field_aliases'; -import { getFunction } from './functions'; -import { derivedFields } from './derived_fields'; - -const getValue = (astNode) => { - if (astNode.type === ast.Types.COLLECTION) return astNode.value.map(getValue); - if (astNode.type === ast.Types.FIELD_NAME) return getFieldAlias(astNode.value) || astNode.value; - if (astNode.type === ast.Types.FUNCTION_CALL) { - const fn = getFunction(astNode.name); - if (!fn) throw new Error(sprintf(__('Unknown function: %{name}'), { name: astNode.name })); - return fn.getFieldName(...getValue(astNode.args)); - } - - return astNode.value; -}; - -export const transformAstToDisplayFields = (astNode) => { - if (astNode.type === ast.Types.COLLECTION) return astNode.value.map(transformAstToDisplayFields); - if (astNode.type === ast.Types.FIELD_NAME) { - const fieldName = getValue(astNode); - const displayField = { - key: fieldName, - label: astNode.alias?.value || toSentenceCase(astNode.value), - name: fieldName, - }; - if (derivedFields[fieldName]) displayField.transform = derivedFields[fieldName]; - return displayField; - } - if (astNode.type === ast.Types.FUNCTION_CALL) { - const args = getValue(astNode.args); - const key = uniqueId(`${astNode.name}_${args.join('_')}_`); - const fn = getFunction(astNode.name); - return { - key, - label: astNode.alias?.value || fn.getFieldLabel(...args), - name: getValue(astNode), - transform: fn.getTransformer(key, ...args), - }; - } - - throw new Error(sprintf(__('Unknown value type: %{type}'), { type: astNode.type })); -}; diff --git a/app/assets/javascripts/glql/core/transformer/data.js b/app/assets/javascripts/glql/core/transformer/data.js deleted file mode 100644 index b564357a581..00000000000 --- a/app/assets/javascripts/glql/core/transformer/data.js +++ /dev/null @@ -1,59 +0,0 @@ -import { omit } from 'lodash'; - -const DATA_SOURCES = ['epics', 'issues', 'mergeRequests', 'workItems']; - -const transformWorkItems = (workItems) => { - for (const workItem of workItems.nodes || []) { - for (const widget of workItem.widgets || []) { - Object.assign(workItem, omit(widget, ['type', '__typename'])); - } - delete workItem.widgets; - } - - return workItems; -}; - -const transformForDataSource = (data) => { - const scope = data.project || data.group; - - let source; - let transformed; - - for (source of DATA_SOURCES) { - if (source in scope) { - transformed = scope[source]; - break; - } - } - - if (source === 'workItems') { - transformed = transformWorkItems(structuredClone(transformed)); - } - - return { source, transformed }; -}; - -const transformField = (data, field) => { - if (field.transform) - return { - ...data, - nodes: data.nodes.map((node) => field.transform(node)), - }; - return data; -}; - -const transformFields = (data, fields) => { - return fields.reduce((acc, field) => transformField(acc, field), data); -}; - -export const transform = (data, config) => { - let source = config.source || 'issues'; - let transformed = data; - - ({ transformed, source } = transformForDataSource(transformed) || {}); - transformed = transformFields(transformed, config.fields); - - // eslint-disable-next-line no-param-reassign - if (source) config.source = source; - return transformed; -}; diff --git a/app/assets/javascripts/glql/core/transformer/derived_fields.js b/app/assets/javascripts/glql/core/transformer/derived_fields.js deleted file mode 100644 index 174affc4543..00000000000 --- a/app/assets/javascripts/glql/core/transformer/derived_fields.js +++ /dev/null @@ -1,6 +0,0 @@ -export const derivedFields = { - lastComment: (node) => ({ - ...node, - lastComment: node.lastComment.nodes[0]?.bodyHtml, - }), -}; diff --git a/app/assets/javascripts/glql/core/transformer/field_aliases.js b/app/assets/javascripts/glql/core/transformer/field_aliases.js deleted file mode 100644 index 1f5a672f7ec..00000000000 --- a/app/assets/javascripts/glql/core/transformer/field_aliases.js +++ /dev/null @@ -1,28 +0,0 @@ -import { glqlWorkItemsFeatureFlagEnabled } from '../../utils/feature_flags'; - -const fieldAliases = { - // We don't want to expose the id (GID) field to the user, so we alias it to iid - id: 'iid', - assignee: 'assignees', - closed: 'closedAt', - created: 'createdAt', - due: 'dueDate', - health: 'healthStatus', - label: 'labels', - updated: 'updatedAt', - description: 'descriptionHtml', - - merged: 'mergedAt', - reviewer: 'reviewers', - merger: 'mergedBy', - approver: 'approvedBy', -}; - -if (glqlWorkItemsFeatureFlagEnabled()) { - Object.assign(fieldAliases, { - epic: 'parent', - start: 'startDate', - }); -} - -export const getFieldAlias = (fieldName) => fieldAliases[fieldName] || fieldName; diff --git a/app/assets/javascripts/glql/core/transformer/functions.js b/app/assets/javascripts/glql/core/transformer/functions.js deleted file mode 100644 index f3c538c61b4..00000000000 --- a/app/assets/javascripts/glql/core/transformer/functions.js +++ /dev/null @@ -1,31 +0,0 @@ -import { __, n__ } from '~/locale'; -import { toSentenceCase } from '../../utils/common'; -import { wildcardMatch } from '../../../lib/utils/text_utility'; - -const functions = { - labels: { - getFieldName: () => 'labels', - getFieldLabel: (...values) => { - const labels = values.map(toSentenceCase).join(', '); - return `${n__('Label', 'Labels', values.length)}: ${labels}`; - }, - getTransformer: - (key, ...values) => - (data) => { - if (values.length > 10) - throw new Error(__('Function `labels` can only take a maximum of 10 parameters.')); - - const filter = (label) => values.some((value) => wildcardMatch(label.title, value)); - return { - ...data, - [key]: { ...data.labels, nodes: data.labels.nodes.filter(filter) }, - labels: { - ...data.labels, - nodes: data.labels.nodes.filter((label) => !filter(label)), - }, - }; - }, - }, -}; - -export const getFunction = (name) => functions[name]; diff --git a/app/assets/javascripts/glql/utils/common.js b/app/assets/javascripts/glql/utils/common.js index 37767812d9a..7a4ca7e12aa 100644 --- a/app/assets/javascripts/glql/utils/common.js +++ b/app/assets/javascripts/glql/utils/common.js @@ -8,7 +8,9 @@ export const extractGroupOrProject = (url = window.location.href) => { const isGroup = fullPath.startsWith('/groups'); fullPath = fullPath.replace(/^\/groups\//, '').replace(/^\//g, ''); - return isGroup ? { group: fullPath } : { project: fullPath }; + if (isGroup) return { group: fullPath }; + if (fullPath) return { project: fullPath }; + return {}; }; export const toSentenceCase = (str) => { diff --git a/app/assets/javascripts/glql/utils/feature_flags.js b/app/assets/javascripts/glql/utils/feature_flags.js index 3f827eff802..522d5773b84 100644 --- a/app/assets/javascripts/glql/utils/feature_flags.js +++ b/app/assets/javascripts/glql/utils/feature_flags.js @@ -6,3 +6,7 @@ export const glqlWorkItemsFeatureFlagEnabled = () => { return Boolean(gon.features?.glqlWorkItems); }; + +export const glqlFeatureFlags = () => ({ + glqlWorkItems: glqlWorkItemsFeatureFlagEnabled(), +}); diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 8b94c0075b6..bc63cc81f47 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -574,25 +574,6 @@ export const uniquifyString = (originalStr, otherStrings, modifier) => return uniqueString === acc ? acc + modifier : acc; }, originalStr); -/** - * Checks if a string contains a search string (with optional wildcards) - * - * @param {String} str String to search within - * @param {String} pattern String to look for (with * representing wildcards) - * - * @returns {Boolean} Whether the string matches the search pattern - */ -export const wildcardMatch = (str, pattern) => { - const lowerPattern = pattern.toLowerCase(); - const lowerStr = str.toLowerCase(); - - if (!pattern.includes('*')) return lowerPattern === lowerStr; - - const escapedPattern = lowerPattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const regex = new RegExp(`^${escapedPattern.replace(/\\\*/g, '.*')}$`); - return regex.test(lowerStr); -}; - export const sha256 = async (str) => { const data = new TextEncoder().encode(str); const hashBuffer = await crypto.subtle.digest('SHA-256', data); diff --git a/app/assets/javascripts/pinia/plugins.js b/app/assets/javascripts/pinia/plugins.js index 846d2ac5e95..53515ded33e 100644 --- a/app/assets/javascripts/pinia/plugins.js +++ b/app/assets/javascripts/pinia/plugins.js @@ -31,7 +31,12 @@ export const globalAccessorPlugin = (context) => { */ // use this only for component migration export const syncWithVuex = (context) => { + let unsubPreviousVuex; + let unsubPreviousPinia; + const syncWith = (config) => { + unsubPreviousVuex?.(); + unsubPreviousPinia?.(); const { store: vuexStore, name: vuexName, @@ -48,7 +53,7 @@ export const syncWithVuex = (context) => { let committing = false; - vuexStore.subscribe( + unsubPreviousVuex = vuexStore.subscribe( (mutation) => { if (committing) return; const { payload, type } = mutation; @@ -64,7 +69,7 @@ export const syncWithVuex = (context) => { { prepend: true }, ); - context.store.$onAction(({ name: mutationName, args }) => { + unsubPreviousPinia = context.store.$onAction(({ name: mutationName, args }) => { if (committing) return; const fullMutationName = namespaced ? `${vuexName}/${mutationName}` : mutationName; // eslint-disable-next-line no-underscore-dangle diff --git a/app/assets/javascripts/vue_shared/access_tokens/components/access_token_statistics.vue b/app/assets/javascripts/vue_shared/access_tokens/components/access_token_statistics.vue index e6592cb640e..788aa71d70e 100644 --- a/app/assets/javascripts/vue_shared/access_tokens/components/access_token_statistics.vue +++ b/app/assets/javascripts/vue_shared/access_tokens/components/access_token_statistics.vue @@ -13,13 +13,14 @@ export default { GlSingleStat, }, computed: { - ...mapState(useAccessTokens, ['statistics']), + ...mapState(useAccessTokens, ['statistics', 'urlParams']), }, methods: { ...mapActions(useAccessTokens, ['fetchTokens', 'setFilters', 'setPage']), handleFilter(filters) { this.setFilters(filters); this.setPage(1); + this.$router.push({ query: this.urlParams }); this.fetchTokens(); }, slugifyStat(stat) { diff --git a/app/assets/javascripts/vue_shared/access_tokens/components/access_tokens.vue b/app/assets/javascripts/vue_shared/access_tokens/components/access_tokens.vue index 51ad8b5a81b..35f49517329 100644 --- a/app/assets/javascripts/vue_shared/access_tokens/components/access_tokens.vue +++ b/app/assets/javascripts/vue_shared/access_tokens/components/access_tokens.vue @@ -63,6 +63,7 @@ export default { 'token', 'tokens', 'total', + 'urlParams', ]), }, created() { @@ -75,7 +76,12 @@ export default { urlRotate: this.accessTokenRotate, urlShow: this.accessTokenShow, }); + this.$router.replace({ query: this.urlParams }); this.fetchTokens(); + window.addEventListener('popstate', this.handlePopState); + }, + beforeDestroy() { + window.removeEventListener('popstate', this.handlePopState); }, methods: { ...mapActions(useAccessTokens, [ @@ -94,19 +100,31 @@ export default { search(filters) { this.setFilters(filters); this.setPage(1); + this.$router.push({ query: this.urlParams }); this.fetchTokens(); }, async pageChanged(page) { this.setPage(page); + this.$router.push({ query: this.urlParams }); + await this.fetchTokens(); + window.scrollTo({ top: 0 }); + }, + async handlePopState() { + const { filters, page, sorting } = initializeValuesFromQuery(); + this.setFilters(filters); + this.setPage(page); + this.setSorting(sorting); await this.fetchTokens(); window.scrollTo({ top: 0 }); }, handleSortChange(value) { this.setSorting({ value, isAsc: this.sorting.isAsc }); + this.$router.push({ query: this.urlParams }); this.fetchTokens(); }, handleSortDirectionChange(isAsc) { this.setSorting({ value: this.sorting.value, isAsc }); + this.$router.push({ query: this.urlParams }); this.fetchTokens(); }, }, diff --git a/app/assets/javascripts/vue_shared/access_tokens/stores/access_tokens.js b/app/assets/javascripts/vue_shared/access_tokens/stores/access_tokens.js index f529045d696..a0f9511ec28 100644 --- a/app/assets/javascripts/vue_shared/access_tokens/stores/access_tokens.js +++ b/app/assets/javascripts/vue_shared/access_tokens/stores/access_tokens.js @@ -11,7 +11,7 @@ import { import { joinPaths } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import { SORT_OPTIONS, DEFAULT_SORT } from '~/access_tokens/constants'; -import { serializeParams, update15DaysFromNow, updateUrlWithQueryParams } from '../utils'; +import { serializeParams, update15DaysFromNow } from '../utils'; /** * @typedef {{type: string, value: {data: string, operator: string}}} Filter @@ -132,7 +132,6 @@ export const useAccessTokens = defineStore('accessTokens', { this.busy = true; try { const url = Api.buildUrl(this.urlShow.replace(':id', this.id)); - updateUrlWithQueryParams({ params: this.params, sort: this.sort }); const { data, perPage, total } = await fetchTokens({ url, params: this.params, @@ -286,5 +285,11 @@ export const useAccessTokens = defineStore('accessTokens', { return isAsc ? sortOption.sort.asc : sortOption.sort.desc; }, + urlParams() { + return { + ...this.params, + sort: this.sort, + }; + }, }, }); diff --git a/app/assets/javascripts/vue_shared/access_tokens/utils.js b/app/assets/javascripts/vue_shared/access_tokens/utils.js index 74ff26b44e6..5e58a2857a2 100644 --- a/app/assets/javascripts/vue_shared/access_tokens/utils.js +++ b/app/assets/javascripts/vue_shared/access_tokens/utils.js @@ -1,5 +1,4 @@ import { getDateInFuture, nDaysAfter, toISODateFormat } from '~/lib/utils/datetime_utility'; -import { setUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { STATISTICS_CONFIG } from '~/access_tokens/constants'; /** @@ -62,17 +61,3 @@ export function update15DaysFromNow(stats = STATISTICS_CONFIG) { return clonedStats; } - -/** - * Sets the URL parameters based on the provided query parameters. - * @param {Object} options - * @param {Object} options.params - * @param {string} options.sort - */ -export function updateUrlWithQueryParams({ params, sort }) { - const queryParams = { ...params, sort }; - updateHistory({ - url: setUrlParams(queryParams, window.location.href, true), - replace: true, - }); -} diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 1f999540b33..b16d55e70a8 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -211,6 +211,7 @@ export default { showSidebar: true, truncationEnabled: true, lastRealtimeUpdatedAt: new Date(), + isRefetching: false, }; }, apollo: { @@ -242,6 +243,10 @@ export default { return data.workspace.workItem ?? {}; }, error() { + if (this.isRefetching) { + this.isRefetching = false; + return; + } this.setEmptyState(); }, result(res) { @@ -869,6 +874,7 @@ export default { const now = new Date(); const staleThreshold = 5 * 60 * 1000; // 5 minutes in milliseconds if (now - this.lastRealtimeUpdatedAt > staleThreshold) { + this.isRefetching = true; this.$apollo.queries.workItem.refetch(); this.lastRealtimeUpdatedAt = now; } diff --git a/app/assets/stylesheets/components/rapid_diffs/text_file_viewers.scss b/app/assets/stylesheets/components/rapid_diffs/text_file_viewers.scss index e4d4aa4de93..e54a2a74d31 100644 --- a/app/assets/stylesheets/components/rapid_diffs/text_file_viewers.scss +++ b/app/assets/stylesheets/components/rapid_diffs/text_file_viewers.scss @@ -142,12 +142,7 @@ z-index: constants.$diff-file-line-number-z-index; } -.rd-line-number:target, -.rd-line-number:target ~ .rd-line-number { - --rd-adjacent-line-border-color: var(--rd-line-border-color); -} - -.rd-hunk-lines:has(> .rd-line-number:target) { +.rd-hunk-lines:target { --rd-line-background-color-override: var(--code-highlighted-line-background-color); // handle empty cells --code-line-number-background-color: var(--code-highlighted-line-background-color); @@ -159,6 +154,10 @@ z-index: constants.$diff-file-highlighted-line-number-z-index; } +.rd-hunk-lines:target .rd-line-number { + --rd-adjacent-line-border-color: var(--rd-line-border-color); +} + // child combinator improves performance of the selector // duplicate .rd-hunk-lines-inline selector to override highlighted color .rd-hunk-lines-parallel:hover > .rd-line-number:not(:empty), diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 44c99b11e13..04e50f07313 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -74,6 +74,9 @@ body.modal-open { overflow: hidden; +} + +body.modal-open:not(.body-fixed-scrollbar) { padding-right: 0 !important; } diff --git a/app/components/rapid_diffs/viewers/text/inline_hunk_component.html.haml b/app/components/rapid_diffs/viewers/text/inline_hunk_component.html.haml index 7a4f6b28cc1..02c9f41fbd8 100644 --- a/app/components/rapid_diffs/viewers/text/inline_hunk_component.html.haml +++ b/app/components/rapid_diffs/viewers/text/inline_hunk_component.html.haml @@ -5,7 +5,8 @@ - if @diff_hunk.lines - @diff_hunk.lines.each do |line| - %tr.rd-hunk-lines.rd-hunk-lines-inline{ data: { testid: 'hunk-lines-inline', hunk_lines: true } } - = render RapidDiffs::Viewers::Text::LineNumberComponent.new(file_hash: @file_hash, file_path: @file_path, line: line, position: :old) - = render RapidDiffs::Viewers::Text::LineNumberComponent.new(file_hash: @file_hash, file_path: @file_path, line: line, position: :new, border: :right) + - line_id = line.id(@file_hash) + %tr.rd-hunk-lines.rd-hunk-lines-inline{ id: line_id, data: { testid: 'hunk-lines-inline', hunk_lines: true } } + = render RapidDiffs::Viewers::Text::LineNumberComponent.new(line: line, line_id: line_id, position: :old) + = render RapidDiffs::Viewers::Text::LineNumberComponent.new(line: line, line_id: line_id, position: :new, border: :right) = render RapidDiffs::Viewers::Text::LineContentComponent.new(line: line, position: nil) diff --git a/app/components/rapid_diffs/viewers/text/line_number_component.html.haml b/app/components/rapid_diffs/viewers/text/line_number_component.html.haml index a493ce69374..30f50c701da 100644 --- a/app/components/rapid_diffs/viewers/text/line_number_component.html.haml +++ b/app/components/rapid_diffs/viewers/text/line_number_component.html.haml @@ -1,5 +1,5 @@ - if visible? - %td.rd-line-number{ id: id, class: border_class, data: { change: change_type, position: @position } } - = link_to '', "##{id}", { class: 'rd-line-link', data: { line_number: line_number }, aria: { label: label } } + %td.rd-line-number{ class: border_class, data: { change: change_type, position: @position } } + = link_to '', "##{@line_id}", { class: 'rd-line-link', data: { line_number: line_number }, aria: { label: label } } - else %td.rd-line-number{ class: border_class, data: { change: change_type, position: @position } } diff --git a/app/components/rapid_diffs/viewers/text/line_number_component.rb b/app/components/rapid_diffs/viewers/text/line_number_component.rb index 1a2b34621b2..5da366ac5d4 100644 --- a/app/components/rapid_diffs/viewers/text/line_number_component.rb +++ b/app/components/rapid_diffs/viewers/text/line_number_component.rb @@ -4,18 +4,13 @@ module RapidDiffs module Viewers module Text class LineNumberComponent < ViewComponent::Base - def initialize(line:, position:, file_hash:, file_path:, border: nil) + def initialize(line:, line_id:, position:, border: nil) @line = line + @line_id = line_id @position = position - @file_hash = file_hash - @file_path = file_path @border = border end - def id - @line.id(@file_hash, @position) - end - def line_number @position == :old ? @line.old_pos : @line.new_pos end diff --git a/app/components/rapid_diffs/viewers/text/parallel_hunk_component.html.haml b/app/components/rapid_diffs/viewers/text/parallel_hunk_component.html.haml index e60f0060139..49326f5c93c 100644 --- a/app/components/rapid_diffs/viewers/text/parallel_hunk_component.html.haml +++ b/app/components/rapid_diffs/viewers/text/parallel_hunk_component.html.haml @@ -3,9 +3,10 @@ %td.rd-hunk-header-buttons= render RapidDiffs::Viewers::Text::ExpandLinesComponent.new(directions: @diff_hunk.header.expand_directions) %td.rd-hunk-header-content{ colspan: '3' }= @diff_hunk.header.text -- @diff_hunk.parallel_lines.each do |pair| - %tr.rd-hunk-lines.rd-hunk-lines-parallel{ data: { testid: 'hunk-lines-parallel', hunk_lines: true } } - - sides(pair).each do |side| - - line, position = side.values_at(:line, :position) - = render RapidDiffs::Viewers::Text::LineNumberComponent.new(file_hash: @file_hash, file_path: @file_path, **side) +- line_pairs.each do |pair| + - line_id, sides = pair.values_at(:line_id, :sides) + %tr.rd-hunk-lines.rd-hunk-lines-parallel{ id: line_id, data: { testid: 'hunk-lines-parallel', hunk_lines: true } } + - sides.each do |side| + - line, position, border = side.values_at(:line, :position, :border) + = render RapidDiffs::Viewers::Text::LineNumberComponent.new(line: line, line_id: line_id, position: position, border: border) = render RapidDiffs::Viewers::Text::LineContentComponent.new(line: line, position: position) diff --git a/app/components/rapid_diffs/viewers/text/parallel_hunk_component.rb b/app/components/rapid_diffs/viewers/text/parallel_hunk_component.rb index d99863cf2e4..f907ce5d1f1 100644 --- a/app/components/rapid_diffs/viewers/text/parallel_hunk_component.rb +++ b/app/components/rapid_diffs/viewers/text/parallel_hunk_component.rb @@ -12,19 +12,28 @@ module RapidDiffs @file_path = file_path end - def sides(line_pair) - [ + def id(line) + line.id(@file_hash) + end + + def line_pairs + @diff_hunk.parallel_lines.map do |pair| { - line: line_pair[:left], - position: :old, - border: :right - }, - { - line: line_pair[:right], - position: :new, - border: :both + line_id: id(pair[:left] || pair[:right]), + sides: [ + { + line: pair[:left], + position: :old, + border: :right + }, + { + line: pair[:right], + position: :new, + border: :both + } + ] } - ] + end end end end diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 01a5dfa7a6a..35afa4a132e 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -196,7 +196,7 @@ class GraphqlController < ApplicationController def any_mutating_query? if multiplex? - multiplex_queries.any? { |q| mutation?(q[:query], q[:operation_name]) } + multiplex_param.any? { |q| mutation?(q[:query], q[:operationName]) } else mutation?(query) end @@ -295,6 +295,7 @@ class GraphqlController < ApplicationController @context ||= { current_user: current_user, is_sessionless_user: api_user, + current_organization: Current.organization, request: request, scope_validator: ::Gitlab::Auth::ScopeValidator.new(api_user, request_authenticator), remove_deprecated: Gitlab::Utils.to_boolean(permitted_params[:remove_deprecated], default: false) diff --git a/app/models/user.rb b/app/models/user.rb index e5a99d588e0..2f7479d400b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -696,6 +696,14 @@ class User < ApplicationRecord project_bot.joins(:user_detail).where(user_detail: { bot_namespace_id: namespace_ids }) end + scope :with_incoming_email_token, ->(token_values) do + where(incoming_email_token: Array.wrap(token_values)) + end + + scope :with_feed_token, ->(token_values) do + where(feed_token: Array.wrap(token_values)) + end + def self.supported_keyset_orderings { id: [:asc, :desc], diff --git a/app/services/container_registry/protection/update_tag_rule_service.rb b/app/services/container_registry/protection/update_tag_rule_service.rb index 2f934f1203e..736662dd5ff 100644 --- a/app/services/container_registry/protection/update_tag_rule_service.rb +++ b/app/services/container_registry/protection/update_tag_rule_service.rb @@ -23,8 +23,6 @@ module ContainerRegistry end def execute - return service_response_error(message: _('Operation not allowed')) if container_protection_tag_rule.immutable? - unless can?(current_user, :admin_container_image, container_protection_tag_rule.project) return service_response_error(message: _('Unauthorized to update a protection rule for container image tags')) end @@ -55,3 +53,5 @@ module ContainerRegistry end end end + +ContainerRegistry::Protection::UpdateTagRuleService.prepend_mod diff --git a/app/views/user_settings/profiles/show.html.haml b/app/views/user_settings/profiles/show.html.haml index 7b90d6519c5..3c2b7b07389 100644 --- a/app/views/user_settings/profiles/show.html.haml +++ b/app/views/user_settings/profiles/show.html.haml @@ -154,7 +154,7 @@ = f.label :mastodon = f.text_field :mastodon, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: "@username@server_name" %li.form-group.gl-form-group - = f.label :github + = f.label :github, s_('Profiles|GitHub') = f.text_field :github, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|username") %li.form-group.gl-form-group = f.label :orcid, s_('Profiles|ORCID') diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 35d712758d6..ef9bfb86ce8 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -1140,10 +1140,10 @@ Settings.cell.database['skip_sequence_alteration'] ||= false # NOTE: `topology_service_client` is the configuration to use going forward as per https://docs.gitlab.com/administration/cells/#configuration # We continue to be backwards compatible and support `topology_service` as a top-level key. Settings.cell['topology_service_client'] ||= Settings.respond_to?(:topology_service) ? Settings.topology_service || {} : {} -Settings.cell.topology_service_client['address'] ||= 'topology-service.gitlab.example.com:443' -Settings.cell.topology_service_client['ca_file'] ||= '/home/git/gitlab/config/topology-service-ca.pem' -Settings.cell.topology_service_client['certificate_file'] ||= '/home/git/gitlab/config/topology-service-cert.pem' -Settings.cell.topology_service_client['private_key_file'] ||= '/home/git/gitlab/config/topology-service-key.pem' +Settings.cell.topology_service_client['address'] ||= 'topology-service.example.com:443' +Settings.cell.topology_service_client['ca_file'] ||= nil +Settings.cell.topology_service_client['certificate_file'] ||= nil +Settings.cell.topology_service_client['private_key_file'] ||= nil # # GitLab KAS diff --git a/db/docs/batched_background_migrations/backfill_pipeline_sd_and_cs_analyzer_project_statuses.yml b/db/docs/batched_background_migrations/backfill_pipeline_sd_and_cs_analyzer_project_statuses.yml new file mode 100644 index 00000000000..5b00ced959d --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_pipeline_sd_and_cs_analyzer_project_statuses.yml @@ -0,0 +1,8 @@ +--- +migration_job_name: BackfillPipelineSdAndCsAnalyzerProjectStatuses +description: Fills analyzer_project_statuses values based on existing types +feature_category: security_asset_inventories +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/195569 +milestone: '18.2' +queued_migration_version: 20250624155101 +finalized_by: diff --git a/db/post_migrate/20250624155101_queue_backfill_pipeline_sd_and_cs_analyzer_project_statuses.rb b/db/post_migrate/20250624155101_queue_backfill_pipeline_sd_and_cs_analyzer_project_statuses.rb new file mode 100644 index 00000000000..753ccb5f8cd --- /dev/null +++ b/db/post_migrate/20250624155101_queue_backfill_pipeline_sd_and_cs_analyzer_project_statuses.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class QueueBackfillPipelineSdAndCsAnalyzerProjectStatuses < Gitlab::Database::Migration[2.3] + milestone '18.2' + + restrict_gitlab_migration gitlab_schema: :gitlab_sec + + MIGRATION = "BackfillPipelineSdAndCsAnalyzerProjectStatuses" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 1000 + SUB_BATCH_SIZE = 100 + + def up + queue_batched_background_migration( + MIGRATION, + :analyzer_project_statuses, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :analyzer_project_statuses, :id, []) + end +end diff --git a/db/schema_migrations/20250624155101 b/db/schema_migrations/20250624155101 new file mode 100644 index 00000000000..5ee517030a7 --- /dev/null +++ b/db/schema_migrations/20250624155101 @@ -0,0 +1 @@ +0107787b64c0abe75658af5d45b5edb16d3b87adb6b9eae5dedd0582c59c28d6 \ No newline at end of file diff --git a/doc/administration/file_hooks.md b/doc/administration/file_hooks.md index dd9ee44b065..7033369950b 100644 --- a/doc/administration/file_hooks.md +++ b/doc/administration/file_hooks.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: File hooks --- diff --git a/doc/administration/monitoring/github_imports.md b/doc/administration/monitoring/github_imports.md index 6b5a582015a..d28f76ea946 100644 --- a/doc/administration/monitoring/github_imports.md +++ b/doc/administration/monitoring/github_imports.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Monitoring GitHub imports --- diff --git a/doc/administration/settings/import_and_export_settings.md b/doc/administration/settings/import_and_export_settings.md index b9c673a9d41..872c9670ec6 100644 --- a/doc/administration/settings/import_and_export_settings.md +++ b/doc/administration/settings/import_and_export_settings.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments gitlab_dedicated: yes title: Import and export settings diff --git a/doc/administration/settings/import_export_rate_limits.md b/doc/administration/settings/import_export_rate_limits.md index 3a6f216ae9d..ea9fba1f6f1 100644 --- a/doc/administration/settings/import_export_rate_limits.md +++ b/doc/administration/settings/import_export_rate_limits.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Rate limits for imports and exports of project and groups --- diff --git a/doc/administration/settings/project_integration_management.md b/doc/administration/settings/project_integration_management.md index ad38468bf70..21836f394d8 100644 --- a/doc/administration/settings/project_integration_management.md +++ b/doc/administration/settings/project_integration_management.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Integration administration --- diff --git a/doc/administration/settings/slack_app.md b/doc/administration/settings/slack_app.md index eda0fadcac7..19639a4bf57 100644 --- a/doc/administration/settings/slack_app.md +++ b/doc/administration/settings/slack_app.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: GitLab for Slack app administration --- diff --git a/doc/administration/system_hooks.md b/doc/administration/system_hooks.md index 9ea0bea809c..b1db7e5d50c 100644 --- a/doc/administration/system_hooks.md +++ b/doc/administration/system_hooks.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments gitlab_dedicated: yes title: System hooks diff --git a/doc/api/_index.md b/doc/api/_index.md index 29ad7813a3c..f037fbfe0af 100644 --- a/doc/api/_index.md +++ b/doc/api/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Extend with GitLab description: Connect GitLab to your tools and workflows to build a customized development environment. diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index f53ae9151c7..66d24c32bda 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: REST API resources --- diff --git a/doc/api/bulk_imports.md b/doc/api/bulk_imports.md index df22fe674e3..a1ecf665291 100644 --- a/doc/api/bulk_imports.md +++ b/doc/api/bulk_imports.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Group and project migration by direct transfer API --- diff --git a/doc/api/graphql/_index.md b/doc/api/graphql/_index.md index 3ec125055ff..b941607ee50 100644 --- a/doc/api/graphql/_index.md +++ b/doc/api/graphql/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Programmatic interaction with GitLab. title: GraphQL API diff --git a/doc/api/graphql/examples.md b/doc/api/graphql/examples.md index 23d4c18c6e9..fda33624dec 100644 --- a/doc/api/graphql/examples.md +++ b/doc/api/graphql/examples.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: GraphQL examples --- diff --git a/doc/api/graphql/getting_started.md b/doc/api/graphql/getting_started.md index 743a2e0968e..764cd812862 100644 --- a/doc/api/graphql/getting_started.md +++ b/doc/api/graphql/getting_started.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Run GraphQL API queries and mutations --- diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 4bb10418769..4298e25ca74 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: GraphQL API resources --- diff --git a/doc/api/graphql/removed_items.md b/doc/api/graphql/removed_items.md index d1a9ef378fb..1a47ebb26f2 100644 --- a/doc/api/graphql/removed_items.md +++ b/doc/api/graphql/removed_items.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: GraphQL API removed items --- diff --git a/doc/api/group_import_export.md b/doc/api/group_import_export.md index 1085eb6997c..36293cd051f 100644 --- a/doc/api/group_import_export.md +++ b/doc/api/group_import_export.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Group import and export API --- diff --git a/doc/api/group_integrations.md b/doc/api/group_integrations.md index 02ad619884b..fd9e503f0fc 100644 --- a/doc/api/group_integrations.md +++ b/doc/api/group_integrations.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Group integrations API --- diff --git a/doc/api/group_placeholder_reassignments.md b/doc/api/group_placeholder_reassignments.md index 186ad27645c..d19d970eeee 100644 --- a/doc/api/group_placeholder_reassignments.md +++ b/doc/api/group_placeholder_reassignments.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Group placeholder reassignments API --- diff --git a/doc/api/group_relations_export.md b/doc/api/group_relations_export.md index 12553475f90..ea407168701 100644 --- a/doc/api/group_relations_export.md +++ b/doc/api/group_relations_export.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Group relations export API --- diff --git a/doc/api/group_webhooks.md b/doc/api/group_webhooks.md index c99dd40bb61..f6ed1c89684 100644 --- a/doc/api/group_webhooks.md +++ b/doc/api/group_webhooks.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Group webhooks API --- diff --git a/doc/api/import.md b/doc/api/import.md index 1c42fd262dc..47b03d3b42e 100644 --- a/doc/api/import.md +++ b/doc/api/import.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Import API --- diff --git a/doc/api/openapi/openapi_interactive.md b/doc/api/openapi/openapi_interactive.md index 906364f68d9..cac2da7b75a 100644 --- a/doc/api/openapi/openapi_interactive.md +++ b/doc/api/openapi/openapi_interactive.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Interactive API documentation --- diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index c2aab544475..0c149e7269d 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Project import and export API --- diff --git a/doc/api/project_integrations.md b/doc/api/project_integrations.md index 2baf21ea905..5606399ad39 100644 --- a/doc/api/project_integrations.md +++ b/doc/api/project_integrations.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Project integrations API --- diff --git a/doc/api/project_relations_export.md b/doc/api/project_relations_export.md index 4cf9c9801f7..f791530af06 100644 --- a/doc/api/project_relations_export.md +++ b/doc/api/project_relations_export.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Project relations export API --- diff --git a/doc/api/project_webhooks.md b/doc/api/project_webhooks.md index 98bc07c6bbe..0918a17b462 100644 --- a/doc/api/project_webhooks.md +++ b/doc/api/project_webhooks.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Project webhooks API --- diff --git a/doc/api/rest/_index.md b/doc/api/rest/_index.md index 31b2115e81f..b2ddbbf71c1 100644 --- a/doc/api/rest/_index.md +++ b/doc/api/rest/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Programmatic interaction with GitLab. title: REST API diff --git a/doc/api/rest/authentication.md b/doc/api/rest/authentication.md index 1c7169194c4..1c54770bb1e 100644 --- a/doc/api/rest/authentication.md +++ b/doc/api/rest/authentication.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Programmatic interaction with GitLab. title: REST API authentication diff --git a/doc/api/rest/deprecations.md b/doc/api/rest/deprecations.md index 2284bb35618..a9353b3e30f 100644 --- a/doc/api/rest/deprecations.md +++ b/doc/api/rest/deprecations.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: REST API deprecations --- diff --git a/doc/api/rest/third_party_clients.md b/doc/api/rest/third_party_clients.md index 9f1d93ae16a..4d6daf0fa7d 100644 --- a/doc/api/rest/third_party_clients.md +++ b/doc/api/rest/third_party_clients.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Programmatic interaction with GitLab. title: REST API third-party clients diff --git a/doc/api/rest/troubleshooting.md b/doc/api/rest/troubleshooting.md index 75c9455b34a..fcadb75ce73 100644 --- a/doc/api/rest/troubleshooting.md +++ b/doc/api/rest/troubleshooting.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Programmatic interaction with GitLab. title: Troubleshooting the REST API diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index 777ace75e91..47fb129370a 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: System hooks API --- diff --git a/doc/ci/variables/_index.md b/doc/ci/variables/_index.md index b6e9e7cd97e..b66e640e53d 100644 --- a/doc/ci/variables/_index.md +++ b/doc/ci/variables/_index.md @@ -596,7 +596,6 @@ You can specify a pipeline variable when you: - Use [push options](../../topics/git/commit.md#push-options-for-gitlab-cicd). - Pass variables to a downstream pipeline by using either the [`variables` keyword](../pipelines/downstream_pipelines.md#pass-cicd-variables-to-a-downstream-pipeline), [`trigger:forward` keyword](../yaml/_index.md#triggerforward) or [`dotenv` variables](../pipelines/downstream_pipelines.md#pass-dotenv-variables-created-in-a-job). -- Specify variables when [running a manual job](../pipelines/_index.md#run-a-pipeline-manually). These variables have [higher precedence](#cicd-variable-precedence) and can override other defined variables, including [predefined variables](predefined_variables.md). diff --git a/doc/development/bulk_import.md b/doc/development/bulk_import.md index 2237f304757..33440a039f8 100644 --- a/doc/development/bulk_import.md +++ b/doc/development/bulk_import.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: Group migration by direct transfer --- diff --git a/doc/development/bulk_imports/contributing.md b/doc/development/bulk_imports/contributing.md index e15cb1126e1..43db04db121 100644 --- a/doc/development/bulk_imports/contributing.md +++ b/doc/development/bulk_imports/contributing.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: Add new relations to the direct transfer importer --- diff --git a/doc/development/export_csv.md b/doc/development/export_csv.md index 5726fbb13be..5005cdf7387 100644 --- a/doc/development/export_csv.md +++ b/doc/development/export_csv.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: Export to CSV --- diff --git a/doc/development/graphql_guide/_index.md b/doc/development/graphql_guide/_index.md index aedfbcb6767..a338223ba22 100644 --- a/doc/development/graphql_guide/_index.md +++ b/doc/development/graphql_guide/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: GraphQL development guidelines --- diff --git a/doc/development/graphql_guide/graphql_pro.md b/doc/development/graphql_guide/graphql_pro.md index d783c346c02..d62f3f120df 100644 --- a/doc/development/graphql_guide/graphql_pro.md +++ b/doc/development/graphql_guide/graphql_pro.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: GraphQL Pro --- diff --git a/doc/development/graphql_guide/monitoring.md b/doc/development/graphql_guide/monitoring.md index 349dd72bb7b..ec8b4a591f8 100644 --- a/doc/development/graphql_guide/monitoring.md +++ b/doc/development/graphql_guide/monitoring.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: Reading GraphQL logs --- diff --git a/doc/development/graphql_guide/pagination.md b/doc/development/graphql_guide/pagination.md index e8f3318726f..51dbb6ba326 100644 --- a/doc/development/graphql_guide/pagination.md +++ b/doc/development/graphql_guide/pagination.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: GraphQL pagination --- diff --git a/doc/development/i18n/_index.md b/doc/development/i18n/_index.md index d1bd2e56bfd..3582f026d0a 100644 --- a/doc/development/i18n/_index.md +++ b/doc/development/i18n/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: Translate GitLab to your language --- diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index 1041816093a..b736aba80d5 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: Internationalization for GitLab --- diff --git a/doc/development/i18n/merging_translations.md b/doc/development/i18n/merging_translations.md index 510de295856..68327b4aad2 100644 --- a/doc/development/i18n/merging_translations.md +++ b/doc/development/i18n/merging_translations.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: Merging translations from Crowdin --- diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index a317c6e7ab5..0d3c394a4ef 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: Proofread Translations --- diff --git a/doc/development/import/principles_of_importer_design.md b/doc/development/import/principles_of_importer_design.md index 5bde1ee76f1..98755a16837 100644 --- a/doc/development/import/principles_of_importer_design.md +++ b/doc/development/import/principles_of_importer_design.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: Principles of Importer Design --- diff --git a/doc/development/import_export.md b/doc/development/import_export.md index 2d03fc7b2e8..5fca29585a8 100644 --- a/doc/development/import_export.md +++ b/doc/development/import_export.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: Import/Export development documentation --- diff --git a/doc/development/integrations/_index.md b/doc/development/integrations/_index.md index d36f644b619..70279e5bfc4 100644 --- a/doc/development/integrations/_index.md +++ b/doc/development/integrations/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. description: Development guidelines for Integrations title: Integration development guidelines diff --git a/doc/development/integrations/jenkins.md b/doc/development/integrations/jenkins.md index bcd87c11f79..9234032cfab 100644 --- a/doc/development/integrations/jenkins.md +++ b/doc/development/integrations/jenkins.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. title: How to run Jenkins in development environment (on macOS) --- diff --git a/doc/development/webhooks.md b/doc/development/webhooks.md index 8a139938b68..c073ec860ca 100644 --- a/doc/development/webhooks.md +++ b/doc/development/webhooks.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/development/development_processes/#development-guidelines-review. description: Development guidelines for webhooks title: Webhooks developer guide diff --git a/doc/integration/_index.md b/doc/integration/_index.md index 5c3d98fc092..082aac82d88 100644 --- a/doc/integration/_index.md +++ b/doc/integration/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Projects, issues, authentication, security providers. title: Integrate with GitLab diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 977a9234535..fa1c2381fe1 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: External issue trackers --- diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md index 328b31bd9e0..8151ee31297 100644 --- a/doc/integration/gmail_action_buttons_for_gitlab.md +++ b/doc/integration/gmail_action_buttons_for_gitlab.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Gmail actions --- diff --git a/doc/integration/trello_power_up.md b/doc/integration/trello_power_up.md index 77acbb78f7e..eb8e8d45ec9 100644 --- a/doc/integration/trello_power_up.md +++ b/doc/integration/trello_power_up.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Trello Power-Ups --- diff --git a/doc/solutions/integrations/servicenow.md b/doc/solutions/integrations/servicenow.md index a93366ab90a..ed6a0df46f0 100644 --- a/doc/solutions/integrations/servicenow.md +++ b/doc/solutions/integrations/servicenow.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: ServiceNow --- diff --git a/doc/user/group/import/_index.md b/doc/user/group/import/_index.md index bdac33df15a..5424a569ef1 100644 --- a/doc/user/group/import/_index.md +++ b/doc/user/group/import/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Migrating GitLab by using direct transfer --- diff --git a/doc/user/group/import/direct_transfer_migrations.md b/doc/user/group/import/direct_transfer_migrations.md index ddc942d5064..fef5abb47f5 100644 --- a/doc/user/group/import/direct_transfer_migrations.md +++ b/doc/user/group/import/direct_transfer_migrations.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Migrate groups and projects by using direct transfer --- diff --git a/doc/user/group/import/migrated_items.md b/doc/user/group/import/migrated_items.md index 6ed80e26523..6d8c20c5eca 100644 --- a/doc/user/group/import/migrated_items.md +++ b/doc/user/group/import/migrated_items.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Items migrated when using direct transfer --- diff --git a/doc/user/group/import/troubleshooting.md b/doc/user/group/import/troubleshooting.md index ed695625a07..9be2dc168b3 100644 --- a/doc/user/group/import/troubleshooting.md +++ b/doc/user/group/import/troubleshooting.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Troubleshooting direct transfer migrations --- diff --git a/doc/user/project/import/_index.md b/doc/user/project/import/_index.md index 9bcb061629e..2492866bb76 100644 --- a/doc/user/project/import/_index.md +++ b/doc/user/project/import/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Import and migrate groups and projects description: Repository migration, third-party repositories, and user contribution mapping. diff --git a/doc/user/project/import/bitbucket.md b/doc/user/project/import/bitbucket.md index bfa730590c4..65b4c91e693 100644 --- a/doc/user/project/import/bitbucket.md +++ b/doc/user/project/import/bitbucket.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Import your project from Bitbucket Cloud --- diff --git a/doc/user/project/import/bitbucket_server.md b/doc/user/project/import/bitbucket_server.md index fd84ac37774..5d6b5e36152 100644 --- a/doc/user/project/import/bitbucket_server.md +++ b/doc/user/project/import/bitbucket_server.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Import your project from Bitbucket Server --- diff --git a/doc/user/project/import/clearcase.md b/doc/user/project/import/clearcase.md index b7db5d76528..43528e39f1e 100644 --- a/doc/user/project/import/clearcase.md +++ b/doc/user/project/import/clearcase.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Migrating from ClearCase --- diff --git a/doc/user/project/import/cvs.md b/doc/user/project/import/cvs.md index 540502d86e1..6ae337b97bf 100644 --- a/doc/user/project/import/cvs.md +++ b/doc/user/project/import/cvs.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Migrating from CVS --- diff --git a/doc/user/project/import/fogbugz.md b/doc/user/project/import/fogbugz.md index 2c089c505c5..6675ea2f5aa 100644 --- a/doc/user/project/import/fogbugz.md +++ b/doc/user/project/import/fogbugz.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Import your project from FogBugz to GitLab --- diff --git a/doc/user/project/import/gitea.md b/doc/user/project/import/gitea.md index 402060b58f7..8f8ced97358 100644 --- a/doc/user/project/import/gitea.md +++ b/doc/user/project/import/gitea.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Import your project from Gitea to GitLab --- diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index f1a12b06a97..c556b6ac0b7 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Import your project from GitHub to GitLab --- diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md index acc7a21cf8e..ec2ddc55660 100644 --- a/doc/user/project/import/manifest.md +++ b/doc/user/project/import/manifest.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Import multiple repositories by uploading a manifest file --- diff --git a/doc/user/project/import/perforce.md b/doc/user/project/import/perforce.md index 93e1560b3f1..5b35b9bf8df 100644 --- a/doc/user/project/import/perforce.md +++ b/doc/user/project/import/perforce.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Migrating from Perforce Helix --- diff --git a/doc/user/project/import/repo_by_url.md b/doc/user/project/import/repo_by_url.md index 701d0012d6b..8120079d817 100644 --- a/doc/user/project/import/repo_by_url.md +++ b/doc/user/project/import/repo_by_url.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Import project from repository by URL --- diff --git a/doc/user/project/import/tfvc.md b/doc/user/project/import/tfvc.md index 54b80b0ad0c..e84cb6c7bc4 100644 --- a/doc/user/project/import/tfvc.md +++ b/doc/user/project/import/tfvc.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Migrate from TFVC to Git --- diff --git a/doc/user/project/import/troubleshooting_github_import.md b/doc/user/project/import/troubleshooting_github_import.md index 146e161f7b6..77f52f29e73 100644 --- a/doc/user/project/import/troubleshooting_github_import.md +++ b/doc/user/project/import/troubleshooting_github_import.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Troubleshooting importing a project from GitHub to GitLab --- diff --git a/doc/user/project/integrations/_index.md b/doc/user/project/integrations/_index.md index de723e21516..2be8adfd7cc 100644 --- a/doc/user/project/integrations/_index.md +++ b/doc/user/project/integrations/_index.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Project integrations --- diff --git a/doc/user/project/integrations/asana.md b/doc/user/project/integrations/asana.md index 366307c8cb2..41df32005ec 100644 --- a/doc/user/project/integrations/asana.md +++ b/doc/user/project/integrations/asana.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Asana --- diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md index 7037ef58a9b..08d2b42bddc 100644 --- a/doc/user/project/integrations/bugzilla.md +++ b/doc/user/project/integrations/bugzilla.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Bugzilla --- diff --git a/doc/user/project/integrations/clickup.md b/doc/user/project/integrations/clickup.md index c50663cc9ef..eb2efdca052 100644 --- a/doc/user/project/integrations/clickup.md +++ b/doc/user/project/integrations/clickup.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: ClickUp --- diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md index 335aabc9afd..bbc4c018971 100644 --- a/doc/user/project/integrations/custom_issue_tracker.md +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Custom issue tracker --- diff --git a/doc/user/project/integrations/discord_notifications.md b/doc/user/project/integrations/discord_notifications.md index b8b5c811ab8..eca0f04763f 100644 --- a/doc/user/project/integrations/discord_notifications.md +++ b/doc/user/project/integrations/discord_notifications.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Discord Notifications --- diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md index 816678d957c..61cd950c55e 100644 --- a/doc/user/project/integrations/emails_on_push.md +++ b/doc/user/project/integrations/emails_on_push.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Emails on push --- diff --git a/doc/user/project/integrations/ewm.md b/doc/user/project/integrations/ewm.md index e2fcf69fa65..cb7d77718c0 100644 --- a/doc/user/project/integrations/ewm.md +++ b/doc/user/project/integrations/ewm.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Engineering Workflow Management (EWM) --- diff --git a/doc/user/project/integrations/gitlab_slack_app_troubleshooting.md b/doc/user/project/integrations/gitlab_slack_app_troubleshooting.md index 53eae72ce7d..50ce95a4bb1 100644 --- a/doc/user/project/integrations/gitlab_slack_app_troubleshooting.md +++ b/doc/user/project/integrations/gitlab_slack_app_troubleshooting.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Troubleshooting GitLab for Slack app --- diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md index 9b2199d6718..ddfb6a1c87a 100644 --- a/doc/user/project/integrations/gitlab_slack_application.md +++ b/doc/user/project/integrations/gitlab_slack_application.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: GitLab for Slack app --- diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md index 638851b5505..d0e663c2e5a 100644 --- a/doc/user/project/integrations/hangouts_chat.md +++ b/doc/user/project/integrations/hangouts_chat.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Google Chat --- diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index 681bcc107db..c1b4b832fab 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: irker (IRC gateway) --- diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index f5217ea7479..2e90e5a9b1c 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Mattermost notifications --- diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md index 483bc32775a..1d960371d71 100644 --- a/doc/user/project/integrations/mattermost_slash_commands.md +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Mattermost slash commands --- diff --git a/doc/user/project/integrations/microsoft_teams.md b/doc/user/project/integrations/microsoft_teams.md index 0bd10e48901..94d5e9d228b 100644 --- a/doc/user/project/integrations/microsoft_teams.md +++ b/doc/user/project/integrations/microsoft_teams.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Microsoft Teams notifications --- diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md index 1e406d79e3a..24f161828bd 100644 --- a/doc/user/project/integrations/mock_ci.md +++ b/doc/user/project/integrations/mock_ci.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Mock CI --- diff --git a/doc/user/project/integrations/phorge.md b/doc/user/project/integrations/phorge.md index 0d36f062a37..78e804c7185 100644 --- a/doc/user/project/integrations/phorge.md +++ b/doc/user/project/integrations/phorge.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Phorge --- diff --git a/doc/user/project/integrations/pipeline_status_emails.md b/doc/user/project/integrations/pipeline_status_emails.md index fe66ee7379d..23083b62882 100644 --- a/doc/user/project/integrations/pipeline_status_emails.md +++ b/doc/user/project/integrations/pipeline_status_emails.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Pipeline status emails --- diff --git a/doc/user/project/integrations/pivotal_tracker.md b/doc/user/project/integrations/pivotal_tracker.md index 826ac934bdf..8944e506ff3 100644 --- a/doc/user/project/integrations/pivotal_tracker.md +++ b/doc/user/project/integrations/pivotal_tracker.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Pivotal Tracker --- diff --git a/doc/user/project/integrations/pumble.md b/doc/user/project/integrations/pumble.md index 22421d0bf0b..a5ae0061540 100644 --- a/doc/user/project/integrations/pumble.md +++ b/doc/user/project/integrations/pumble.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Pumble --- diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md index 5432fb9b80c..3e80cba0a87 100644 --- a/doc/user/project/integrations/redmine.md +++ b/doc/user/project/integrations/redmine.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Redmine --- diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md index e5711c48402..b06daa7ff5b 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Slack notifications (deprecated) --- diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md index 969f3ae9cf6..bfef662b019 100644 --- a/doc/user/project/integrations/slack_slash_commands.md +++ b/doc/user/project/integrations/slack_slash_commands.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Slack slash commands --- diff --git a/doc/user/project/integrations/squash_tm.md b/doc/user/project/integrations/squash_tm.md index ec05b778f1a..99b928d9bd2 100644 --- a/doc/user/project/integrations/squash_tm.md +++ b/doc/user/project/integrations/squash_tm.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Squash TM --- diff --git a/doc/user/project/integrations/telegram.md b/doc/user/project/integrations/telegram.md index 62c473331a5..edc99b88134 100644 --- a/doc/user/project/integrations/telegram.md +++ b/doc/user/project/integrations/telegram.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Telegram --- diff --git a/doc/user/project/integrations/unify_circuit.md b/doc/user/project/integrations/unify_circuit.md index 11c778a2840..d01b1b16212 100644 --- a/doc/user/project/integrations/unify_circuit.md +++ b/doc/user/project/integrations/unify_circuit.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Unify Circuit --- diff --git a/doc/user/project/integrations/webex_teams.md b/doc/user/project/integrations/webex_teams.md index 41f07020259..4f6672f6d80 100644 --- a/doc/user/project/integrations/webex_teams.md +++ b/doc/user/project/integrations/webex_teams.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Webex Teams --- diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md index e1466f28722..f8b86b3ca97 100644 --- a/doc/user/project/integrations/webhook_events.md +++ b/doc/user/project/integrations/webhook_events.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Webhook events --- diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index bb55825df60..5d1fff053b4 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Custom HTTP callbacks, used to send events. title: Webhooks diff --git a/doc/user/project/integrations/webhooks_troubleshooting.md b/doc/user/project/integrations/webhooks_troubleshooting.md index b0645d8f177..94f44eba6f1 100644 --- a/doc/user/project/integrations/webhooks_troubleshooting.md +++ b/doc/user/project/integrations/webhooks_troubleshooting.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Custom HTTP callbacks, used to send events. title: Troubleshooting webhooks diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md index 9c805081ebe..2b475c4723d 100644 --- a/doc/user/project/integrations/youtrack.md +++ b/doc/user/project/integrations/youtrack.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: YouTrack --- diff --git a/doc/user/project/integrations/zentao.md b/doc/user/project/integrations/zentao.md index 5466aaa2b73..d2d2aa48c52 100644 --- a/doc/user/project/integrations/zentao.md +++ b/doc/user/project/integrations/zentao.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: ZenTao (deprecated) --- diff --git a/doc/user/project/issues/csv_import.md b/doc/user/project/issues/csv_import.md index c817905c69f..91c1024237d 100644 --- a/doc/user/project/issues/csv_import.md +++ b/doc/user/project/issues/csv_import.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Importing issues from CSV --- diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 2af6f44e595..2f22cfc3925 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Migrate projects and groups by using file exports --- diff --git a/doc/user/project/settings/import_export_troubleshooting.md b/doc/user/project/settings/import_export_troubleshooting.md index 9ce268692d0..7a5c840eab4 100644 --- a/doc/user/project/settings/import_export_troubleshooting.md +++ b/doc/user/project/settings/import_export_troubleshooting.md @@ -1,6 +1,6 @@ --- -stage: Foundations -group: Import and Integrate +stage: Create +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Troubleshooting file export project migrations --- diff --git a/keeps/delete_old_feature_flags.rb b/keeps/delete_old_feature_flags.rb index b9bd38955d7..1edaf3be503 100644 --- a/keeps/delete_old_feature_flags.rb +++ b/keeps/delete_old_feature_flags.rb @@ -50,23 +50,46 @@ module Keeps end def can_remove_ff?(feature_flag, identifiers, latest_feature_flag_status) - intended_to_rollout_by_date = feature_flag.intended_to_rollout_by - if intended_to_rollout_by_date.present? - rollout_date = parse_date(intended_to_rollout_by_date) - if !rollout_date.nil? && rollout_date.future? - @logger.puts "#{feature_flag.name} cannot be removed, intended rollout date is #{intended_to_rollout_by_date}" - return false - elsif rollout_date.nil? - message = "#{feature_flag.name} intended_to_rollout_by #{intended_to_rollout_by_date}" - @logger.puts "#{message}, is ignored as it cannot be parsed." - end + return false unless valid_feature_flag_status?(feature_flag, latest_feature_flag_status) + + # Check if feature flag has ready for removal label - this bypasses most validation checks + if has_ready_for_removal_label?(feature_flag) + @logger.puts "#{feature_flag.name} has 'feature flag::ready for removal' label, bypassing validation checks" + return true end - if feature_flag.milestone.nil? - @logger.puts "#{feature_flag.name} has no milestone set!" + return false unless valid_rollout_date?(feature_flag) + return false unless valid_milestone?(feature_flag) + return false unless within_milestone_cutoff?(feature_flag, latest_feature_flag_status) + return false unless matches_filter_identifiers?(identifiers) + + true + end + + def valid_rollout_date?(feature_flag) + intended_to_rollout_by_date = feature_flag.intended_to_rollout_by + return true unless intended_to_rollout_by_date.present? + + rollout_date = parse_date(intended_to_rollout_by_date) + if rollout_date.nil? + message = "#{feature_flag.name} intended_to_rollout_by #{intended_to_rollout_by_date}" + @logger.puts "#{message}, is ignored as it cannot be parsed." + elsif rollout_date.future? + @logger.puts "#{feature_flag.name} cannot be removed, intended rollout date is #{intended_to_rollout_by_date}" return false end + true + end + + def valid_milestone?(feature_flag) + return true unless feature_flag.milestone.nil? + + @logger.puts "#{feature_flag.name} has no milestone set!" + false + end + + def valid_feature_flag_status?(feature_flag, latest_feature_flag_status) if latest_feature_flag_status.nil? @logger.puts "#{feature_flag.name} cannot be removed as we cannot get the status from the rollout issue." return false @@ -77,29 +100,43 @@ module Keeps return false end + true + end + + def within_milestone_cutoff?(feature_flag, latest_feature_flag_status) cutoff = if latest_feature_flag_status == :disabled CUTOFF_MILESTONE_FOR_DISABLED_FLAG else CUTOFF_MILESTONE_FOR_ENABLED_FLAG end - unless milestones_helper.before_cuttoff?(milestone: feature_flag.milestone, milestones_ago: cutoff) - @logger.puts "#{feature_flag.name} cannot be removed as it is after the cutoff." - return false - end + return true if milestones_helper.before_cuttoff?(milestone: feature_flag.milestone, milestones_ago: cutoff) - unless matches_filter_identifiers?(identifiers) - @logger.puts "#{feature_flag.name} cannot be removed as it is not matching passed filter." - return false - end - - true + @logger.puts "#{feature_flag.name} cannot be removed as it is after the cutoff." + false end # rubocop:disable Gitlab/DocumentationLinks/HardcodedUrl -- Not running inside rails application def build_description(feature_flag, latest_feature_flag_status) + ready_for_removal = has_ready_for_removal_label?(feature_flag) + + introduction_text = if ready_for_removal + "This feature flag was introduced in #{feature_flag.milestone} and has been " \ + "marked as ready for removal with the `~\"feature flag::ready for removal\"` " \ + "label, bypassing the standard milestone cutoff requirements." + else + cutoff_count = if latest_feature_flag_status == :enabled + CUTOFF_MILESTONE_FOR_ENABLED_FLAG + else + CUTOFF_MILESTONE_FOR_DISABLED_FLAG + end + + "This feature flag was introduced in #{feature_flag.milestone}, which is " \ + "more than #{cutoff_count} milestones ago." + end + <<~MARKDOWN - This feature flag was introduced in #{feature_flag.milestone}, which is more than #{latest_feature_flag_status == :enabled ? CUTOFF_MILESTONE_FOR_ENABLED_FLAG : CUTOFF_MILESTONE_FOR_DISABLED_FLAG} milestones ago. + #{introduction_text} As part of our process we want to ensure [feature flags don't stay too long in the codebase](https://docs.gitlab.com/ee/development/feature_flags/#types-of-feature-flags). @@ -118,7 +155,8 @@ module Keeps It is possible that this MR will still need some changes to remove references to the feature flag in the code. At the moment the `gitlab-housekeeper` is not always capable of removing all references so you must check the diff and pipeline failures to confirm if there are any issues. It is the responsibility of ~"#{feature_flag.group}" to push those changes to this branch. - If they are already removing this feature flag in another merge request then they can just close this merge request and add `intended_to_rollout_by` date in the yml file. + + **Note:** If you do not want to remove this feature flag at this time, you can add an `intended_to_rollout_by_date` attribute in the feature flag YAML file to prevent automated removal. ## TODO for the reviewers before merging this MR - [ ] See the status of the rollout by checking #{feature_flag_rollout_issue_url(feature_flag)}, #{format(FEATURE_FLAG_LOG_ISSUES_URL, feature_flag_name: feature_flag.name)} @@ -139,6 +177,7 @@ module Keeps change.changelog_type = 'removed' change.title = "Delete the `#{feature_flag.name}` feature flag" change.identifiers = identifiers + change.description = build_description(feature_flag, latest_feature_flag_status) FileUtils.rm(feature_flag.path) change.changed_files = [feature_flag.path] @@ -152,8 +191,6 @@ module Keeps return change end - change.description = build_description(feature_flag, latest_feature_flag_status) - change.labels = [ 'automation:feature-flag-removal', 'maintenance::removal', @@ -208,7 +245,6 @@ module Keeps unless user_message @logger.puts "#{feature_flag.name}: No prompt generated for #{file}, skipping" - failed_files << file next end @@ -230,7 +266,6 @@ module Keeps end if failed_files.any? - @logger.puts "#{feature_flag.name}: Successfully processed #{success_count} files" @logger.puts "failed on #{failed_files.size} files" @logger.puts "Failed files: #{failed_files.join(', ')}" end @@ -293,14 +328,29 @@ module Keeps ) # rubocop:enable Gitlab/HttpV2 - unless (200..299).cover?(response.code) - raise Error, + unless response.success? + @logger.puts( "Get URL: #{rollout_issue_url} Failed with response code: #{response.code} and body:\n#{response.body}" + ) + return end Gitlab::Json.parse(response.body, symbolize_names: true) end + def has_ready_for_removal_label?(feature_flag) + rollout_issue_url = feature_flag_rollout_issue_url(feature_flag) + return false if rollout_issue_url == MISSING_URL_PLACEHOLDER + + rollout_issue = get_rollout_issue(rollout_issue_url) + return false unless rollout_issue + + rollout_issue[:labels].any?('feature flag::ready for removal') + rescue StandardError => e + @logger.puts "Error checking ready for removal label for #{feature_flag.name}: #{e.message}" + false + end + def get_latest_feature_flag_status(feature_flag) return :enabled if feature_flag.default_enabled diff --git a/keeps/helpers/ai_editor.rb b/keeps/helpers/ai_editor.rb index 58b89cd8e16..0f9341e8047 100644 --- a/keeps/helpers/ai_editor.rb +++ b/keeps/helpers/ai_editor.rb @@ -79,8 +79,8 @@ module Keeps changes = extract_changes_from_blocks(ai_response) if changes.empty? - puts "No valid code blocks found in AI response for #{file}" - return false + puts "No valid code blocks found in AI response for #{file} assuming no fixes needed." + return true end changes.each do |change| diff --git a/keeps/prompts/remove_feature_flags.rb b/keeps/prompts/remove_feature_flags.rb index 72b5a98dd76..089c4cc05da 100644 --- a/keeps/prompts/remove_feature_flags.rb +++ b/keeps/prompts/remove_feature_flags.rb @@ -5,6 +5,8 @@ module Keeps class RemoveFeatureFlags attr_reader :logger + SKIP_PATTERNS_FOR_MARKDOWN = ['doc-locale/', 'changelog', 'graphql/reference/_index.md'].freeze + def initialize(logger) @logger = logger end @@ -26,6 +28,7 @@ module Keeps prompt_vue(feature_flag, file, flag_enabled) else logger.puts("Unexpected file extension in #{file} referencing feature flag #{feature_flag.name}. Skipping.") + nil end end @@ -756,7 +759,7 @@ module Keeps def skip_markdown_file?(file) file_path = file.to_s.downcase - file_path.include?('doc-locale') + SKIP_PATTERNS_FOR_MARKDOWN.any? { |pattern| file_path.include?(pattern) } end end end diff --git a/lib/gitlab/background_migration/backfill_pipeline_sd_and_cs_analyzer_project_statuses.rb b/lib/gitlab/background_migration/backfill_pipeline_sd_and_cs_analyzer_project_statuses.rb new file mode 100644 index 00000000000..8216cb7129c --- /dev/null +++ b/lib/gitlab/background_migration/backfill_pipeline_sd_and_cs_analyzer_project_statuses.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + class BackfillPipelineSdAndCsAnalyzerProjectStatuses < BatchedMigrationJob + feature_category :security_asset_inventories + operation_name :backfill_pipeline_sd_and_cs_analyzer_project_statuses + + def perform; end + end + end +end + +Gitlab::BackgroundMigration::BackfillPipelineSdAndCsAnalyzerProjectStatuses.prepend_mod diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index bbd5881f916..bab92fb5eb4 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -136,12 +136,14 @@ module Gitlab rich_text ? rich_text[1..] : text(prefix: false) end - def id(file_hash, side) + def id(file_hash) return if meta? - prefix = side == :old ? "L" : "R" - position = side == :old ? old_pos : new_pos - "line_#{file_hash[0..8]}_#{prefix}#{position}" + hash = file_hash[0..8] + + return "line_#{hash}_A#{new_pos}" if added? + + "line_#{hash}_#{old_pos}" end def legacy_id(file_path) diff --git a/lib/gitlab/topology_service_client/base_service.rb b/lib/gitlab/topology_service_client/base_service.rb index fefc168dbf0..2ff7ae8e96f 100644 --- a/lib/gitlab/topology_service_client/base_service.rb +++ b/lib/gitlab/topology_service_client/base_service.rb @@ -23,8 +23,18 @@ module Gitlab end def service_credentials - # mTls will be implemented later in Phase 5: https://gitlab.com/groups/gitlab-org/-/epics/14281 - :this_channel_is_insecure + config = Gitlab.config.cell.topology_service_client + + ca_file, key_file, cert_file = config.values_at('ca_file', 'private_key_file', 'certificate_file') + + return GRPC::Core::ChannelCredentials.new unless key_file && cert_file + return GRPC::Core::ChannelCredentials.new unless File.exist?(key_file) && File.exist?(cert_file) + + ca_cert_content = File.read(ca_file) if ca_file && File.exist?(ca_file) + + GRPC::Core::ChannelCredentials.new(ca_cert_content, File.read(key_file), File.read(cert_file)) + rescue Errno::EACCES => e + raise "Failed to read certificate files: #{e.message}" end def topology_service_address diff --git a/lib/sidebars/projects/menus/monitor_menu.rb b/lib/sidebars/projects/menus/monitor_menu.rb index 04b5eb2be35..5eda2e0ab62 100644 --- a/lib/sidebars/projects/menus/monitor_menu.rb +++ b/lib/sidebars/projects/menus/monitor_menu.rb @@ -63,9 +63,10 @@ module Sidebars end def alert_management_menu_item - unless can?(context.current_user, :read_alert_management_alert, context.project) - return ::Sidebars::NilMenuItem.new(item_id: :alert_management) - end + should_hide_menu = Feature.enabled?(:hide_incident_management_features, context.project) || + !can?(context.current_user, :read_alert_management_alert, context.project) + + return ::Sidebars::NilMenuItem.new(item_id: :incidents) if should_hide_menu ::Sidebars::MenuItem.new( title: _('Alerts'), @@ -77,8 +78,8 @@ module Sidebars end def incidents_menu_item - should_hide_menu = !can?(context.current_user, :read_issue, context.project) || - Feature.enabled?(:hide_incident_management_features, context.project) + should_hide_menu = Feature.enabled?(:hide_incident_management_features, context.project) || + !can?(context.current_user, :read_issue, context.project) return ::Sidebars::NilMenuItem.new(item_id: :incidents) if should_hide_menu diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake index 125d10aba33..f140ff0e530 100644 --- a/lib/tasks/gitlab/tw/codeowners.rake +++ b/lib/tasks/gitlab/tw/codeowners.rake @@ -54,7 +54,7 @@ namespace :tw do CodeOwnerRule.new('Gitaly', '@eread'), CodeOwnerRule.new('Global Search', '@ashrafkhamis'), CodeOwnerRule.new('Remote Development', '@brendan777'), - CodeOwnerRule.new('Import and Integrate', '@ashrafkhamis'), + CodeOwnerRule.new('Import', '@ashrafkhamis'), CodeOwnerRule.new('Knowledge', '@msedlakjakubowski'), # CodeOwnerRule.new('MLOps', ''), # CodeOwnerRule.new('Mobile Devops', ''), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3c847accd67..14abf0ff662 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4067,6 +4067,9 @@ msgstr "" msgid "AdminAIPoweredFeatures|Feature" msgstr "" +msgid "AdminAIPoweredFeatures|Features" +msgstr "" + msgid "AdminAIPoweredFeatures|GitLab Default" msgstr "" @@ -4082,6 +4085,9 @@ msgstr "" msgid "AdminAIPoweredFeatures|Select a self-hosted model" msgstr "" +msgid "AdminAIPoweredFeatures|Select the model for the feature" +msgstr "" + msgid "AdminAiPoweredFeatures|%{selected_model} is incompatible with the %{title} feature" msgstr "" @@ -16069,6 +16075,9 @@ msgstr "" msgid "ComplianceReport|Show old report" msgstr "" +msgid "ComplianceReport|There was a problem fetching compliance frameworks." +msgstr "" + msgid "ComplianceReport|This compliance framework's compliance pipeline has been migrated to a pipeline execution policy." msgstr "" @@ -26824,9 +26833,6 @@ msgstr "" msgid "Fetching incoming email" msgstr "" -msgid "Field alias must be of type `String`." -msgstr "" - msgid "File" msgstr "" @@ -27480,9 +27486,6 @@ msgstr "" msgid "Full log" msgstr "" -msgid "Function `labels` can only take a maximum of 10 parameters." -msgstr "" - msgid "Fuzz testing" msgstr "" @@ -36020,9 +36023,7 @@ msgid "LFSStatus|Enabled" msgstr "" msgid "Label" -msgid_plural "Labels" -msgstr[0] "" -msgstr[1] "" +msgstr "" msgid "Label %{labelName} was not found" msgstr "" @@ -44657,12 +44658,6 @@ msgstr "" msgid "Parent item set successfully." msgstr "" -msgid "Parse error: Expected `%{expected}`, but got `%{got}`." -msgstr "" - -msgid "Parse error: Unexpected input near `%{input}`." -msgstr "" - msgid "Participants" msgstr "" @@ -47697,6 +47692,9 @@ msgstr "" msgid "Profiles|Generate New PIN" msgstr "" +msgid "Profiles|GitHub" +msgstr "" + msgid "Profiles|GitLab is unable to verify your identity automatically. For security purposes, you must set a password by %{openingTag}resetting your password%{closingTag} to delete your account." msgstr "" @@ -65936,15 +65934,9 @@ msgstr "" msgid "Unknown format" msgstr "" -msgid "Unknown function: %{name}" -msgstr "" - msgid "Unknown user" msgstr "" -msgid "Unknown value type: %{type}" -msgstr "" - msgid "Unless otherwise agreed to in writing with GitLab, by selecting \"Add License\" you agree that your use of GitLab Software is subject to the %{eula_link_start}Terms of Service%{eula_link_end}." msgstr "" @@ -66061,9 +66053,6 @@ msgstr "" msgid "Unsubscribes from notifications." msgstr "" -msgid "Unsupported node type for alias: %{type}" -msgstr "" - msgid "Unsupported options '%{options}' for exec command '%{command}'. Only '%{supported_options}' are supported." msgstr "" @@ -73681,9 +73670,6 @@ msgstr "" msgid "features adopted" msgstr "" -msgid "field name" -msgstr "" - msgid "file" msgid_plural "files" msgstr[0] "" @@ -73709,9 +73695,6 @@ msgstr[1] "" msgid "frontmatter" msgstr "" -msgid "function name" -msgstr "" - msgid "group" msgstr "" @@ -75038,9 +75021,6 @@ msgstr "" msgid "snippet" msgstr "" -msgid "something to parse" -msgstr "" - msgid "source" msgstr "" @@ -75068,9 +75048,6 @@ msgstr "" msgid "starts on %{timebox_start_date}" msgstr "" -msgid "string" -msgstr "" - msgid "structure is too large. Maximum size is %{max_size} characters" msgstr "" diff --git a/package.json b/package.json index bf6ca282f96..f444f9441e7 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "@gitlab/duo-ui": "^8.22.1", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.3.0", - "@gitlab/query-language-rust": "0.9.2", + "@gitlab/query-language-rust": "0.11.1", "@gitlab/svgs": "3.137.0", "@gitlab/ui": "114.8.1", "@gitlab/vue-router-vue3": "npm:vue-router@4.5.1", diff --git a/spec/components/rapid_diffs/viewers/text/inline_hunk_component_spec.rb b/spec/components/rapid_diffs/viewers/text/inline_hunk_component_spec.rb index b51b65bea54..489df8241a2 100644 --- a/spec/components/rapid_diffs/viewers/text/inline_hunk_component_spec.rb +++ b/spec/components/rapid_diffs/viewers/text/inline_hunk_component_spec.rb @@ -7,12 +7,7 @@ RSpec.describe RapidDiffs::Viewers::Text::InlineHunkComponent, type: :component, let(:lines) { diff_file.diff_lines_with_match_tail } let(:old_line) { lines.find { |line| line.type == 'old' } } let(:new_line) { lines.find { |line| line.type == 'new' } } - let(:hunk) do - Gitlab::Diff::ViewerHunk.new( - header: Gitlab::Diff::ViewerHunkHeader.new(lines.first, 0, 0), - lines: lines.drop(1) - ) - end + let(:hunk) { diff_file.viewer_hunks.first } it "renders header" do render_component @@ -27,20 +22,13 @@ RSpec.describe RapidDiffs::Viewers::Text::InlineHunkComponent, type: :component, end end - it "renders line id" do - old_line_id = old_line.id(diff_file.file_hash, :old) - new_line_id = new_line.id(diff_file.file_hash, :new) + it "renders line links" do render_component - expect(page).to have_selector("##{old_line_id}") - expect(page).to have_selector("##{new_line_id}") - end - - it "renders line link" do - old_line_id = old_line.id(diff_file.file_hash, :old) - new_line_id = new_line.id(diff_file.file_hash, :new) - render_component - expect(page).to have_selector("a[href='##{old_line_id}']") - expect(page).to have_selector("a[href='##{new_line_id}']") + hunk.lines.each do |line| + id = line.id(diff_file.file_hash) + expect(page).to have_selector("a[href='##{id}']") + expect(page).to have_selector("##{id}") + end end it "renders expand up" do diff --git a/spec/components/rapid_diffs/viewers/text/line_number_component_spec.rb b/spec/components/rapid_diffs/viewers/text/line_number_component_spec.rb index 1cfbd0a8efb..13a49da2e79 100644 --- a/spec/components/rapid_diffs/viewers/text/line_number_component_spec.rb +++ b/spec/components/rapid_diffs/viewers/text/line_number_component_spec.rb @@ -7,6 +7,7 @@ RSpec.describe RapidDiffs::Viewers::Text::LineNumberComponent, type: :component, let_it_be(:old_line) { diff_file.diff_lines_with_match_tail.find { |line| line.type == 'old' } } let_it_be(:new_line) { diff_file.diff_lines_with_match_tail.find { |line| line.type == 'new' } } let_it_be(:meta_line) { diff_file.diff_lines_with_match_tail.find { |line| line.type == 'match' } } + let(:line_id) { 'line_id' } let(:link) { page.find('a') } let(:td) { page.find('td') } @@ -45,7 +46,7 @@ RSpec.describe RapidDiffs::Viewers::Text::LineNumberComponent, type: :component, expect(link.text).to eq('') expect(link[:'data-line-number']).to eq(old_line.old_pos.to_s) expect(link[:'aria-label']).to eq("Removed line #{old_line.old_pos}") - expect(td[:id]).to eq(old_line.id(diff_file.file_hash, :old)) + expect(link[:href]).to end_with(line_id) expect(page).to have_selector('[data-position="old"]') end @@ -54,7 +55,7 @@ RSpec.describe RapidDiffs::Viewers::Text::LineNumberComponent, type: :component, expect(link.text).to eq('') expect(link[:'data-line-number']).to eq(new_line.new_pos.to_s) expect(link[:'aria-label']).to eq("Added line #{old_line.new_pos}") - expect(td[:id]).to eq(new_line.id(diff_file.file_hash, :new)) + expect(link[:href]).to end_with(line_id) expect(page).to have_selector('[data-position="new"]') end @@ -62,9 +63,8 @@ RSpec.describe RapidDiffs::Viewers::Text::LineNumberComponent, type: :component, render_inline( described_class.new( line: line, - position: position, - file_hash: diff_file.file_hash, - file_path: diff_file.file_path + line_id: line_id, + position: position ) ) end diff --git a/spec/components/rapid_diffs/viewers/text/parallel_hunk_component_spec.rb b/spec/components/rapid_diffs/viewers/text/parallel_hunk_component_spec.rb index e7f0fa7390e..b4c9d17ffc4 100644 --- a/spec/components/rapid_diffs/viewers/text/parallel_hunk_component_spec.rb +++ b/spec/components/rapid_diffs/viewers/text/parallel_hunk_component_spec.rb @@ -7,12 +7,7 @@ RSpec.describe RapidDiffs::Viewers::Text::ParallelHunkComponent, type: :componen let(:lines) { diff_file.diff_lines_with_match_tail } let(:old_line) { lines.find { |line| line.type == 'old' } } let(:new_line) { lines.find { |line| line.type == 'new' } } - let(:hunk) do - Gitlab::Diff::ViewerHunk.new( - header: Gitlab::Diff::ViewerHunkHeader.new(lines.first, 0, 0), - lines: lines.drop(1) - ) - end + let(:hunk) { diff_file.viewer_hunks.first } it "renders header" do render_component @@ -27,20 +22,14 @@ RSpec.describe RapidDiffs::Viewers::Text::ParallelHunkComponent, type: :componen end end - it "renders line id" do - old_line_id = old_line.id(diff_file.file_hash, :old) - new_line_id = new_line.id(diff_file.file_hash, :new) + it "renders line links" do render_component - expect(page).to have_selector("##{old_line_id}") - expect(page).to have_selector("##{new_line_id}") - end - - it "renders line link" do - old_line_id = old_line.id(diff_file.file_hash, :old) - new_line_id = new_line.id(diff_file.file_hash, :new) - render_component - expect(page).to have_selector("a[href='##{old_line_id}']") - expect(page).to have_selector("a[href='##{new_line_id}']") + hunk.parallel_lines.each do |pair| + line = pair[:left] || pair[:right] + id = line.id(diff_file.file_hash) + expect(page).to have_selector("a[href='##{id}']") + expect(page).to have_selector("##{id}") + end end it "renders expand up" do diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 909212998d9..70e9d56f37f 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe GraphqlController, feature_category: :integrations do +RSpec.describe GraphqlController, :with_current_organization, feature_category: :integrations do include GraphqlHelpers include Auth::DpopTokenHelper @@ -565,6 +565,12 @@ RSpec.describe GraphqlController, feature_category: :integrations do end end + it 'includes Current.organization context' do + post :execute + + expect(assigns(:context)[:current_organization]).to eq(current_organization) + end + it 'includes request object in context' do post :execute diff --git a/spec/features/monitor_sidebar_link_spec.rb b/spec/features/monitor_sidebar_link_spec.rb index 2d56ba9bc58..c0fb19086a6 100644 --- a/spec/features/monitor_sidebar_link_spec.rb +++ b/spec/features/monitor_sidebar_link_spec.rb @@ -52,7 +52,7 @@ RSpec.describe 'Monitor dropdown sidebar', :js, feature_category: :shared do end end - shared_examples 'incident menu visibility based on feature flag' do + shared_examples 'monitor sub menu visibility based on feature flag' do context "when hide_incident_management_features feature is enabled" do before do stub_feature_flags(hide_incident_management_features: true) @@ -63,9 +63,17 @@ RSpec.describe 'Monitor dropdown sidebar', :js, feature_category: :shared do click_button('Monitor') expect(page).not_to have_link('Incidents', href: project_incidents_path(project)) end + + it 'does not show the alert menu' do + visit project_issues_path(project) + click_button('Monitor') + expect(page).not_to have_link('Alerts', href: project_alert_management_index_path(project)) + end end context "when hide_incident_management_features feature is disabled" do + let(:role) { :developer } + before do stub_feature_flags(hide_incident_management_features: false) end @@ -75,6 +83,12 @@ RSpec.describe 'Monitor dropdown sidebar', :js, feature_category: :shared do click_button('Monitor') expect(page).to have_link('Incidents', href: project_incidents_path(project)) end + + it 'shows the alert menu' do + visit project_issues_path(project) + click_button('Monitor') + expect(page).to have_link('Alerts', href: project_alert_management_index_path(project)) + end end end @@ -136,7 +150,7 @@ RSpec.describe 'Monitor dropdown sidebar', :js, feature_category: :shared do end it_behaves_like 'shows common Monitor menu item based on the access level' - it_behaves_like 'incident menu visibility based on feature flag' + it_behaves_like 'monitor sub menu visibility based on feature flag' end context 'when user has reporter role' do @@ -159,7 +173,7 @@ RSpec.describe 'Monitor dropdown sidebar', :js, feature_category: :shared do end it_behaves_like 'shows common Monitor menu item based on the access level' - it_behaves_like 'incident menu visibility based on feature flag' + it_behaves_like 'monitor sub menu visibility based on feature flag' end context 'when user has developer role' do @@ -181,7 +195,7 @@ RSpec.describe 'Monitor dropdown sidebar', :js, feature_category: :shared do end it_behaves_like 'shows common Monitor menu item based on the access level' - it_behaves_like 'incident menu visibility based on feature flag' + it_behaves_like 'monitor sub menu visibility based on feature flag' end context 'when user has maintainer role' do @@ -203,6 +217,6 @@ RSpec.describe 'Monitor dropdown sidebar', :js, feature_category: :shared do end it_behaves_like 'shows common Monitor menu item based on the access level' - it_behaves_like 'incident menu visibility based on feature flag' + it_behaves_like 'monitor sub menu visibility based on feature flag' end end diff --git a/spec/frontend/glql/core/parser/combinators_spec.js b/spec/frontend/glql/core/parser/combinators_spec.js deleted file mode 100644 index 1fe364083fa..00000000000 --- a/spec/frontend/glql/core/parser/combinators_spec.js +++ /dev/null @@ -1,372 +0,0 @@ -import { - tag, - tagNoCase, - regex, - seq, - alt, - many, - optional, - whitespace, - token, -} from '~/glql/core/parser/combinators'; - -describe('Parser combinators', () => { - describe('tag', () => { - it('should parse a string successfully', () => { - const parser = tag('hello'); - const result = parser.run('hello world'); - expect(result).toEqual({ - success: true, - value: 'hello', - rest: ' world', - }); - }); - - it('should fail when string does not match', () => { - const parser = tag('hello'); - const result = parser.run('world'); - expect(result).toEqual({ - success: false, - expected: 'hello', - got: 'world', - }); - }); - }); - - describe('tagNoCase', () => { - it('should parse a string case-insensitively', () => { - const parser = tagNoCase('hello'); - const result = parser.run('HELLO world'); - expect(result).toEqual({ - success: true, - value: 'HELLO', - rest: ' world', - }); - }); - - it('should fail when string does not match', () => { - const parser = tagNoCase('hello'); - const result = parser.run('world'); - expect(result).toEqual({ - success: false, - expected: 'hello', - got: 'world', - }); - }); - - it('should match mixed case strings', () => { - const parser = tagNoCase('hello'); - const result = parser.run('HeLlO there'); - expect(result).toEqual({ - success: true, - value: 'HeLlO', - rest: ' there', - }); - }); - - it('should return the actual input case in the value', () => { - const parser = tagNoCase('select'); - const result = parser.run('SELECT * FROM table'); - expect(result).toEqual({ - success: true, - value: 'SELECT', - rest: ' * FROM table', - }); - }); - }); - - describe('regex', () => { - it('should parse a regex successfully', () => { - const parser = regex(/^\d+/, 'number'); - const result = parser.run('123abc'); - expect(result).toEqual({ - success: true, - value: '123', - rest: 'abc', - }); - }); - - it('should fail when regex does not match', () => { - const parser = regex(/^\d+/, 'number'); - const result = parser.run('abc123'); - expect(result).toEqual({ - success: false, - expected: 'number', - got: 'abc123', - }); - }); - }); - - describe('seq', () => { - it('should parse a simple sequence successfully', () => { - const parser = seq(tag('hello'), tag(' '), tag('world')); - const result = parser.run('hello world!'); - expect(result).toEqual({ - success: true, - value: ['hello', ' ', 'world'], - rest: '!', - }); - }); - - it('should parse a complex sequence with different parser types', () => { - const parser = seq( - tag('start'), - whitespace, - regex(/^\d+/, 'number'), - optional(tag('!')), - many(tag('a')), - ); - const result = parser.run('start 123!aaa end'); - expect(result).toEqual({ - success: true, - value: ['start', ' ', '123', '!', ['a', 'a', 'a']], - rest: ' end', - }); - }); - - it('should fail when any parser in the sequence fails', () => { - const parser = seq(tag('hello'), tag(' '), tag('world'), tag('!')); - const result = parser.run('hello world'); - expect(result).toEqual({ - success: false, - expected: '!', - got: '', - }); - }); - - it('should handle empty input correctly', () => { - const parser = seq(tag('hello'), tag(' '), tag('world')); - const result = parser.run(''); - expect(result).toEqual({ - success: false, - expected: 'hello', - got: '', - }); - }); - }); - - describe('alt', () => { - it('should parse alternatives successfully', () => { - const parser = alt(tag('hello'), tag('hi'), tag('hey')); - const result = parser.run('hi there'); - expect(result).toEqual({ - success: true, - value: 'hi', - rest: ' there', - }); - }); - - it('should try all alternatives and succeed with the first match', () => { - const parser = alt( - seq(tag('hello'), tag(' '), tag('world')), - seq(tag('hi'), tag(' '), tag('there')), - seq(tag('hey'), tag(' '), tag('you')), - ); - const result = parser.run('hi there friend'); - expect(result).toEqual({ - success: true, - value: ['hi', ' ', 'there'], - rest: ' friend', - }); - }); - - it('should fail when no alternative matches', () => { - const parser = alt(tag('hello'), tag('hi'), tag('hey')); - const result = parser.run('greetings'); - expect(result).toEqual({ - success: false, - expected: 'something to parse', - got: 'greetings', - }); - }); - - it('should handle complex alternatives with different parser types', () => { - const parser = alt( - seq(tag('start'), whitespace, regex(/^\d+/, 'number')), - seq(tag('begin'), whitespace, many(tag('a'))), - token(tag('end')), - ); - const result = parser.run('begin aaa'); - expect(result).toEqual({ - success: true, - value: ['begin', ' ', ['a', 'a', 'a']], - rest: '', - }); - }); - }); - - describe('many', () => { - it('should parse multiple occurrences successfully', () => { - const parser = many(tag('a')); - const result = parser.run('aaab'); - expect(result).toEqual({ - success: true, - value: ['a', 'a', 'a'], - rest: 'b', - }); - }); - - it('should return an empty array when no matches', () => { - const parser = many(tag('a')); - const result = parser.run('bbb'); - expect(result).toEqual({ - success: true, - value: [], - rest: 'bbb', - }); - }); - - it('should parse complex repeated patterns', () => { - const parser = many(seq(tag('('), regex(/^[^)]+/, 'content'), tag(')'))); - const result = parser.run('(hello)(world)(!)extra'); - expect(result).toEqual({ - success: true, - value: [ - ['(', 'hello', ')'], - ['(', 'world', ')'], - ['(', '!', ')'], - ], - rest: 'extra', - }); - }); - - it('should handle nested many parsers', () => { - const parser = many(seq(tag('['), many(regex(/^[^\]]+/, 'item')), tag(']'))); - const result = parser.run('[a][b c][d e f]rest'); - expect(result).toEqual({ - success: true, - value: [ - ['[', ['a'], ']'], - ['[', ['b c'], ']'], - ['[', ['d e f'], ']'], - ], - rest: 'rest', - }); - }); - - it('should work with whitespace and tokens', () => { - const parser = many(token(regex(/^[a-z]+/, 'word'))); - const result = parser.run(' hello world !'); - expect(result).toEqual({ - success: true, - value: ['hello', 'world'], - rest: ' !', - }); - }); - }); - - describe('optional', () => { - it('should parse optional element when present', () => { - const parser = optional(tag('a')); - const result = parser.run('ab'); - expect(result).toEqual({ - success: true, - value: 'a', - rest: 'b', - }); - }); - - it('should return null when optional element is not present', () => { - const parser = optional(tag('a')); - const result = parser.run('b'); - expect(result).toEqual({ - success: true, - value: null, - rest: 'b', - }); - }); - }); - - describe('Parser', () => { - it('should map parser results', () => { - const parser = tag('hello').map((value) => value.toUpperCase()); - const result = parser.run('hello world'); - expect(result).toEqual({ - success: true, - value: 'HELLO', - rest: ' world', - }); - }); - - it('should chain parsers', () => { - const parser = tag('hello').chain((value) => tag(` ${value}`)); - const result = parser.run('hello hello'); - expect(result).toEqual({ - success: true, - value: ' hello', - rest: '', - }); - }); - }); - - describe('whitespace', () => { - it('should parse whitespace successfully', () => { - const result = whitespace.run(' hello'); - expect(result).toEqual({ - success: true, - value: ' ', - rest: 'hello', - }); - }); - - it('should fail when no whitespace is present', () => { - const result = whitespace.run('hello'); - expect(result).toEqual({ - success: false, - expected: 'whitespace', - got: 'hello', - }); - }); - - it('should parse different types of whitespace', () => { - const result = whitespace.run(' \t\n\rhello'); - expect(result).toEqual({ - success: true, - value: ' \t\n\r', - rest: 'hello', - }); - }); - }); - - describe('token', () => { - it('should parse a token with leading whitespace', () => { - const parser = token(tag('hello')); - const result = parser.run(' hello world'); - expect(result).toEqual({ - success: true, - value: 'hello', - rest: ' world', - }); - }); - - it('should parse a token without leading whitespace', () => { - const parser = token(tag('hello')); - const result = parser.run('hello world'); - expect(result).toEqual({ - success: true, - value: 'hello', - rest: ' world', - }); - }); - - it('should fail when the token is not present', () => { - const parser = token(tag('hello')); - const result = parser.run(' world'); - expect(result).toEqual({ - success: false, - expected: 'hello', - got: 'world', - }); - }); - - it('should work with different types of parsers', () => { - const parser = token(regex(/^\d+/, 'number')); - const result = parser.run(' \t\n123abc'); - expect(result).toEqual({ - success: true, - value: '123', - rest: 'abc', - }); - }); - }); -}); diff --git a/spec/frontend/glql/core/parser/config_spec.js b/spec/frontend/glql/core/parser/config_spec.js deleted file mode 100644 index 9794e765528..00000000000 --- a/spec/frontend/glql/core/parser/config_spec.js +++ /dev/null @@ -1,29 +0,0 @@ -import { parseYAMLConfig } from '~/glql/core/parser/config'; - -describe('parseYAMLConfig', () => { - it('parses the frontmatter and returns an object', () => { - const frontmatter = 'fields: title, assignees, dueDate\ndisplay: list'; - - expect(parseYAMLConfig(frontmatter)).toEqual({ - fields: [ - { name: 'title', label: 'Title', key: 'title' }, - { name: 'assignees', label: 'Assignees', key: 'assignees' }, - { name: 'dueDate', label: 'Due date', key: 'dueDate' }, - ], - display: 'list', - }); - }); - - it('returns default fields if none are provided', () => { - const frontmatter = 'display: list'; - - expect(parseYAMLConfig(frontmatter, { fields: ['title', 'assignees', 'dueDate'] })).toEqual({ - fields: [ - { name: 'title', label: 'Title', key: 'title' }, - { name: 'assignees', label: 'Assignees', key: 'assignees' }, - { name: 'dueDate', label: 'Due date', key: 'dueDate' }, - ], - display: 'list', - }); - }); -}); diff --git a/spec/frontend/glql/core/parser/fields_spec.js b/spec/frontend/glql/core/parser/fields_spec.js deleted file mode 100644 index 9d6a2cbb804..00000000000 --- a/spec/frontend/glql/core/parser/fields_spec.js +++ /dev/null @@ -1,103 +0,0 @@ -import { parseFields } from '~/glql/core/parser/fields'; -import * as ast from '~/glql/core/parser/ast'; - -describe('GLQL Fields Parser', () => { - describe('parseFields', () => { - it('parses a single field name', () => { - const result = parseFields('title'); - expect(result).toEqual(ast.collection(ast.fieldName('title'))); - }); - - it('parses multiple field names', () => { - const result = parseFields('title,description,createdAt'); - expect(result).toEqual( - ast.collection( - ast.fieldName('title'), - ast.fieldName('description'), - ast.fieldName('createdAt'), - ), - ); - }); - - it('parses a function call', () => { - const result = parseFields('labels("bug")'); - expect(result).toEqual( - ast.collection(ast.functionCall('labels', ast.collection(ast.string('bug')))), - ); - }); - - it('parses a function call with multiple arguments', () => { - const result = parseFields('labels("bug", "feature")'); - expect(result).toEqual( - ast.collection( - ast.functionCall('labels', ast.collection(ast.string('bug'), ast.string('feature'))), - ), - ); - }); - - it('parses a mix of field names and function calls', () => { - const result = parseFields('title,labels("bug"),description'); - expect(result).toEqual( - ast.collection( - ast.fieldName('title'), - ast.functionCall('labels', ast.collection(ast.string('bug'))), - ast.fieldName('description'), - ), - ); - }); - - it('handles whitespace', () => { - const result = parseFields(' title , labels( "bug" ) , description '); - expect(result).toEqual( - ast.collection( - ast.fieldName('title'), - ast.functionCall('labels', ast.collection(ast.string('bug'))), - ast.fieldName('description'), - ), - ); - }); - - it('handles aliases correctly', () => { - const result = parseFields('title as "t", labels("bug") as "l", description as "d"'); - expect(result).toEqual( - ast.collection( - ast.fieldName('title').withAlias(ast.string('t')), - ast.functionCall('labels', ast.collection(ast.string('bug'))).withAlias(ast.string('l')), - ast.fieldName('description').withAlias(ast.string('d')), - ), - ); - }); - - it('parses AS token for aliases in a case-insesitive manner', () => { - const result = parseFields('title As "t", description, createdAt AS "c"'); - expect(result).toEqual( - ast.collection( - ast.fieldName('title').withAlias(ast.string('t')), - ast.fieldName('description'), - ast.fieldName('createdAt').withAlias(ast.string('c')), - ), - ); - }); - - it('handles aliases with whitespace', () => { - const result = parseFields( - ' title as "t" , labels( "bug" ) as "l" , description as "d" ', - ); - expect(result).toEqual( - ast.collection( - ast.fieldName('title').withAlias(ast.string('t')), - ast.functionCall('labels', ast.collection(ast.string('bug'))).withAlias(ast.string('l')), - ast.fieldName('description').withAlias(ast.string('d')), - ), - ); - }); - - it('throws an error for invalid input', () => { - expect(() => parseFields('title,')).toThrow('Parse error'); - }); - - it('throws an error for unclosed function call', () => { - expect(() => parseFields('labels("bug"')).toThrow('Parse error'); - }); - }); -}); diff --git a/spec/frontend/glql/core/parser/index_spec.js b/spec/frontend/glql/core/parser/index_spec.js deleted file mode 100644 index e25ae85be51..00000000000 --- a/spec/frontend/glql/core/parser/index_spec.js +++ /dev/null @@ -1,200 +0,0 @@ -import { parseQueryTextWithFrontmatter, parse } from '~/glql/core/parser'; - -describe('parseQueryTextWithFrontmatter', () => { - it('separates the presentation layer from the query and returns an object', () => { - const text = `--- -fields: title, assignees, dueDate -display: list ---- -assignee = currentUser()`; - - expect(parseQueryTextWithFrontmatter(text)).toEqual({ - frontmatter: 'fields: title, assignees, dueDate\ndisplay: list', - query: 'assignee = currentUser()', - }); - }); - - it('returns empty frontmatter if no frontmatter is present', () => { - const text = 'assignee = currentUser()'; - - expect(parseQueryTextWithFrontmatter(text)).toEqual({ - frontmatter: '', - query: 'assignee = currentUser()', - }); - }); -}); - -describe('parse', () => { - beforeEach(() => { - gon.current_username = 'root'; - }); - - it('parses a simple query correctly', async () => { - expect(await parse('assignee = currentUser()')).toMatchInlineSnapshot(` -{ - "config": { - "display": "list", - "fields": [ - { - "key": "title", - "label": "Title", - "name": "title", - }, - ], - }, - "query": "query GLQL { - issues(assigneeUsernames: "root", first: 100) { - nodes { - id - iid - title - webUrl - reference - state - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - count - } -} -", - "variables": [], -} -`); - }); - - it('parses a query with frontmatter correctly', async () => { - expect( - await parse(` ---- -fields: title, assignees, dueDate -display: table ---- -assignee = currentUser()`), - ).toMatchInlineSnapshot(` -{ - "config": { - "display": "table", - "fields": [ - { - "key": "title", - "label": "Title", - "name": "title", - }, - { - "key": "assignees", - "label": "Assignees", - "name": "assignees", - }, - { - "key": "dueDate", - "label": "Due date", - "name": "dueDate", - }, - ], - }, - "query": "query GLQL { - issues(assigneeUsernames: "root", first: 100) { - nodes { - id - iid - title - webUrl - reference - state - assignees { - nodes { - id - avatarUrl - username - name - webUrl - } - } - dueDate - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - count - } -} -", - "variables": [], -} -`); - }); - - it('parses a YAML based query correctly', async () => { - expect( - await parse(` -fields: title, assignees, dueDate -display: table -limit: 20 -query: assignee = currentUser() -`), - ).toMatchInlineSnapshot(` -{ - "config": { - "display": "table", - "fields": [ - { - "key": "title", - "label": "Title", - "name": "title", - }, - { - "key": "assignees", - "label": "Assignees", - "name": "assignees", - }, - { - "key": "dueDate", - "label": "Due date", - "name": "dueDate", - }, - ], - "limit": 20, - }, - "query": "query GLQL { - issues(assigneeUsernames: "root", first: 20) { - nodes { - id - iid - title - webUrl - reference - state - assignees { - nodes { - id - avatarUrl - username - name - webUrl - } - } - dueDate - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - count - } -} -", - "variables": [], -} -`); - }); -}); diff --git a/spec/frontend/glql/core/parser/query_spec.js b/spec/frontend/glql/core/parser/query_spec.js deleted file mode 100644 index cae11fcd151..00000000000 --- a/spec/frontend/glql/core/parser/query_spec.js +++ /dev/null @@ -1,102 +0,0 @@ -import { parse, print } from 'graphql'; -import { parseQuery } from '~/glql/core/parser/query'; -import { MOCK_FIELDS } from '../../mock_data'; - -const prettify = (query) => print(parse(query)); - -describe('GLQL Query Parser', () => { - describe('parseQuery', () => { - beforeEach(() => { - gon.current_username = 'foobar'; - }); - - afterEach(() => { - delete gon.current_username; - }); - - it('parses a simple query by converting it to GraphQL', async () => { - const query = 'assignee = currentUser()'; - const config = { fields: MOCK_FIELDS, limit: 50 }; - const { query: result } = await parseQuery(query, config); - - expect(prettify(result)).toMatchInlineSnapshot(` -"query GLQL { - issues(assigneeUsernames: "foobar", first: 50) { - nodes { - id - iid - title - webUrl - reference - state - author { - id - avatarUrl - username - name - webUrl - } - description - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - count - } -}" -`); - }); - - it('handles complex queries with multiple conditions', async () => { - const query = 'assignee = currentUser() AND label IN ("bug", "feature")'; - const config = { fields: MOCK_FIELDS, limit: 50 }; - const { query: result } = await parseQuery(query, config); - - expect(prettify(result)).toMatchInlineSnapshot(` -"query GLQL { - issues( - assigneeUsernames: "foobar" - or: {labelNames: ["bug", "feature"]} - first: 50 - ) { - nodes { - id - iid - title - webUrl - reference - state - author { - id - avatarUrl - username - name - webUrl - } - description - } - pageInfo { - startCursor - endCursor - hasNextPage - hasPreviousPage - } - count - } -}" -`); - }); - - it('throws an error for invalid queries', async () => { - const query = 'invalid query syntax'; - const config = { fields: MOCK_FIELDS, limit: 100 }; - - await expect(parseQuery(query, config)).rejects.toThrow( - 'Unexpected `query syntax`, expected operator (one of IN, =, !=, >, or <)', - ); - }); - }); -}); diff --git a/spec/frontend/glql/core/parser_spec.js b/spec/frontend/glql/core/parser_spec.js new file mode 100644 index 00000000000..4eb4e57212d --- /dev/null +++ b/spec/frontend/glql/core/parser_spec.js @@ -0,0 +1,296 @@ +import { parse as parseGraphQL, print } from 'graphql'; +import { + parseQueryTextWithFrontmatter, + parse, + parseQuery, + parseYAMLConfig, +} from '~/glql/core/parser'; + +const prettify = (query) => print(parseGraphQL(query)); + +const MOCK_FIELDS = 'title, author, state, description'; + +describe('parseQueryTextWithFrontmatter', () => { + it('separates the presentation layer from the query and returns an object', () => { + const text = `--- +fields: title, assignees, dueDate +display: list +--- +assignee = currentUser()`; + + expect(parseQueryTextWithFrontmatter(text)).toEqual({ + frontmatter: 'fields: title, assignees, dueDate\ndisplay: list', + query: 'assignee = currentUser()', + }); + }); + + it('returns empty frontmatter if no frontmatter is present', () => { + const text = 'assignee = currentUser()'; + + expect(parseQueryTextWithFrontmatter(text)).toEqual({ + frontmatter: '', + query: 'assignee = currentUser()', + }); + }); +}); + +describe('parse', () => { + beforeEach(() => { + gon.current_username = 'root'; + }); + + it('parses a simple query correctly', async () => { + expect(await parse('assignee = currentUser()')).toMatchInlineSnapshot(` +{ + "config": { + "display": "list", + "fields": "title", + }, + "query": "query GLQL { + issues(assigneeUsernames: "root") { + nodes { + id + iid + title + webUrl + reference + state + title + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + count + } +} +", + "variables": [], +} +`); + }); + + it('parses a query with frontmatter correctly', async () => { + expect( + await parse(` +--- +fields: title, assignees, dueDate +display: table +--- +assignee = currentUser()`), + ).toMatchInlineSnapshot(` +{ + "config": { + "display": "table", + "fields": "title, assignees, dueDate", + }, + "query": "query GLQL { + issues(assigneeUsernames: "root") { + nodes { + id + iid + title + webUrl + reference + state + title + assignees { + nodes { + id + avatarUrl + username + name + webUrl + } + } + dueDate + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + count + } +} +", + "variables": [], +} +`); + }); + + it('parses a YAML based query correctly', async () => { + expect( + await parse(` +fields: title, assignees, dueDate +display: table +limit: 20 +query: assignee = currentUser() +`), + ).toMatchInlineSnapshot(` +{ + "config": { + "display": "table", + "fields": "title, assignees, dueDate", + "limit": 20, + }, + "query": "query GLQL { + issues(assigneeUsernames: "root", first: 20) { + nodes { + id + iid + title + webUrl + reference + state + title + assignees { + nodes { + id + avatarUrl + username + name + webUrl + } + } + dueDate + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + count + } +} +", + "variables": [], +} +`); + }); +}); + +describe('parseYAMLConfig', () => { + it('parses the frontmatter and returns an object', () => { + const frontmatter = 'fields: title, assignees, dueDate\ndisplay: list'; + + expect(parseYAMLConfig(frontmatter)).toEqual({ + fields: 'title, assignees, dueDate', + display: 'list', + }); + }); + + it('returns default fields if none are provided', () => { + const frontmatter = 'display: list'; + + expect(parseYAMLConfig(frontmatter)).toEqual({ + fields: 'title', + display: 'list', + }); + }); +}); + +describe('parseQuery', () => { + beforeEach(() => { + gon.current_username = 'foobar'; + }); + + afterEach(() => { + delete gon.current_username; + }); + + it('parses a simple query by converting it to GraphQL', async () => { + const query = 'assignee = currentUser()'; + const config = { fields: MOCK_FIELDS, limit: 50 }; + const { query: result } = await parseQuery(query, config); + + expect(prettify(result)).toMatchInlineSnapshot(` +"query GLQL { + issues(assigneeUsernames: "foobar", first: 50) { + nodes { + id + iid + title + webUrl + reference + state + title + author { + id + avatarUrl + username + name + webUrl + } + state + descriptionHtml + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + count + } +}" +`); + }); + + it('handles complex queries with multiple conditions', async () => { + const query = 'assignee = currentUser() AND label IN ("bug", "feature")'; + const config = { fields: MOCK_FIELDS, limit: 50, project: 'gitlab-org/gitlab' }; + const { query: result } = await parseQuery(query, config); + + expect(prettify(result)).toMatchInlineSnapshot(` +"query GLQL { + project(fullPath: "gitlab-org/gitlab") { + issues( + assigneeUsernames: "foobar" + or: {labelNames: ["bug", "feature"]} + first: 50 + ) { + nodes { + id + iid + title + webUrl + reference + state + title + author { + id + avatarUrl + username + name + webUrl + } + state + descriptionHtml + } + pageInfo { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + count + } + } +}" +`); + }); + + it('throws an error for invalid queries', async () => { + const query = 'invalid query syntax'; + const config = { fields: MOCK_FIELDS, limit: 100 }; + + await expect(parseQuery(query, config)).rejects.toThrow( + 'Unexpected `query syntax`, expected operator (one of IN, =, !=, >, or <)', + ); + }); +}); diff --git a/spec/frontend/glql/core/transformer/ast_spec.js b/spec/frontend/glql/core/transformer/ast_spec.js deleted file mode 100644 index 187409273d4..00000000000 --- a/spec/frontend/glql/core/transformer/ast_spec.js +++ /dev/null @@ -1,109 +0,0 @@ -import { transformAstToDisplayFields } from '~/glql/core/transformer/ast'; -import * as ast from '~/glql/core/parser/ast'; -import { derivedFields } from '~/glql/core/transformer/derived_fields'; - -describe('transformAstToDisplayFields', () => { - it('transforms a single field name', () => { - const input = ast.fieldName('title'); - const result = transformAstToDisplayFields(input); - expect(result).toEqual({ - key: 'title', - label: 'Title', - name: 'title', - }); - }); - - it('transforms multiple field names', () => { - const input = ast.collection( - ast.fieldName('title'), - ast.fieldName('description'), - ast.fieldName('createdAt'), - ); - const result = transformAstToDisplayFields(input); - expect(result).toEqual([ - { key: 'title', label: 'Title', name: 'title' }, - { key: 'descriptionHtml', label: 'Description', name: 'descriptionHtml' }, - { key: 'createdAt', label: 'Created at', name: 'createdAt' }, - ]); - }); - - it('transforms multiple field names with aliases correctly', () => { - const input = ast.collection( - ast.fieldName('title').withAlias(ast.string('Custom Title')), - ast.fieldName('description').withAlias(ast.string('Details')), - ast.fieldName('createdAt').withAlias(ast.string('Created')), - ); - - const result = transformAstToDisplayFields(input); - expect(result).toEqual([ - { key: 'title', label: 'Custom Title', name: 'title' }, - { key: 'descriptionHtml', label: 'Details', name: 'descriptionHtml' }, - { key: 'createdAt', label: 'Created', name: 'createdAt' }, - ]); - }); - - it('transforms multiple field names with aliases', () => { - const input = ast.collection( - ast.fieldName('assignee'), - ast.fieldName('due'), - ast.fieldName('closed'), - ast.fieldName('health'), - ); - const result = transformAstToDisplayFields(input); - expect(result).toEqual([ - { key: 'assignees', label: 'Assignee', name: 'assignees' }, - { key: 'dueDate', label: 'Due', name: 'dueDate' }, - { key: 'closedAt', label: 'Closed', name: 'closedAt' }, - { key: 'healthStatus', label: 'Health', name: 'healthStatus' }, - ]); - }); - - it('transforms a function call with multiple arguments', () => { - const input = ast.functionCall( - 'labels', - ast.collection(ast.string('bug'), ast.string('feature'), ast.string('test')), - ); - const result = transformAstToDisplayFields(input); - expect(result).toMatchObject({ - key: expect.stringMatching(/^labels_bug_feature_test_/), - label: 'Labels: Bug, Feature, Test', - name: expect.any(String), - transform: expect.any(Function), - }); - }); - - it('transforms a mix of field names and function calls', () => { - const input = ast.collection( - ast.fieldName('title'), - ast.functionCall('labels', ast.collection(ast.string('bug'))), - ast.fieldName('description'), - ); - const result = transformAstToDisplayFields(input); - expect(result).toEqual([ - { key: 'title', label: 'Title', name: 'title' }, - { - key: expect.stringMatching(/^labels_bug_/), - label: 'Label: Bug', - name: expect.any(String), - transform: expect.any(Function), - }, - { key: 'descriptionHtml', label: 'Description', name: 'descriptionHtml' }, - ]); - }); - - it('gets transformer for derived field', () => { - const input = ast.fieldName('lastComment'); - const result = transformAstToDisplayFields(input); - expect(result).toEqual({ - key: 'lastComment', - label: 'Last comment', - name: 'lastComment', - transform: derivedFields.lastComment, - }); - }); - - it('throws an error for unknown AST node types', () => { - const input = { type: 'unknown', value: 'test' }; - expect(() => transformAstToDisplayFields(input)).toThrow('Unknown value type: unknown'); - }); -}); diff --git a/spec/frontend/glql/core/transformer/data_spec.js b/spec/frontend/glql/core/transformer/data_spec.js deleted file mode 100644 index 9f2bc2ca333..00000000000 --- a/spec/frontend/glql/core/transformer/data_spec.js +++ /dev/null @@ -1,170 +0,0 @@ -import { transform } from '~/glql/core/transformer/data'; -import * as functions from '~/glql/core/transformer/functions'; -import { derivedFields } from '~/glql/core/transformer/derived_fields'; - -const MOCK_LABELS1 = { nodes: [{ title: 'bug' }] }; -const MOCK_LABELS2 = { nodes: [{ title: 'feature' }] }; - -const MOCK_ISSUES = { - issues: { - nodes: [ - { id: '1', title: 'Lorem ipsum', labels: MOCK_LABELS1 }, - { id: '2', title: 'Dolor sit amet', labels: MOCK_LABELS2 }, - ], - }, -}; - -const MOCK_EPICS = { - epics: { - nodes: [ - { id: '1', title: 'Lorem ipsum', labels: MOCK_LABELS1 }, - { id: '2', title: 'Dolor sit amet', labels: MOCK_LABELS2 }, - ], - }, -}; - -const MOCK_ISSUES_WITH_LAST_COMMENT = { - issues: { - nodes: [ - { id: '1', title: 'Lorem ipsum', lastComment: { nodes: [] } }, - { id: '2', title: 'Dolor sit amet', lastComment: { nodes: [{ bodyHtml: 'Hello' }] } }, - ], - }, -}; - -const MOCK_MERGE_REQUESTS = { - mergeRequests: { - nodes: [ - { id: '1', title: 'Lorem ipsum', labels: MOCK_LABELS1 }, - { id: '2', title: 'Dolor sit amet', labels: MOCK_LABELS2 }, - ], - }, -}; - -const MOCK_WORK_ITEMS = { - workItems: { - nodes: [ - { - id: '1', - title: 'Lorem ipsum', - widgets: [ - {}, - {}, - {}, - { __typename: 'WorkItemWidgetLabels', type: 'LABELS', labels: MOCK_LABELS1 }, - ], - }, - { - id: '2', - title: 'Dolor sit amet', - widgets: [ - {}, - {}, - {}, - { __typename: 'WorkItemWidgetLabels', type: 'LABELS', labels: MOCK_LABELS2 }, - ], - }, - ], - }, -}; - -const MOCK_WORK_ITEMS_WITHOUT_WIDGETS = { - workItems: { - nodes: [ - { id: '1', title: 'Lorem ipsum' }, - { id: '2', title: 'Dolor sit amet' }, - ], - }, -}; - -describe('GLQL Data Transformer', () => { - describe('transform', () => { - it.each` - sourceType | mockQuery - ${'issues'} | ${MOCK_ISSUES} - ${'epics'} | ${MOCK_EPICS} - ${'mergeRequests'} | ${MOCK_MERGE_REQUESTS} - ${'workItems'} | ${MOCK_WORK_ITEMS} - `('extracts data for $sourceType source', ({ mockQuery }) => { - const mockData = { project: mockQuery }; - const mockConfig = { - fields: [ - { key: 'title', name: 'title' }, - { - key: 'labels_bug', - name: 'labels', - transform: functions.getFunction('labels').getTransformer('labels_bug', 'bug'), - }, - ], - }; - - const result = transform(mockData, mockConfig); - - expect(result).toEqual({ - nodes: [ - { - id: '1', - title: 'Lorem ipsum', - labels_bug: { nodes: [{ title: 'bug' }] }, - labels: { nodes: [] }, - }, - { - id: '2', - title: 'Dolor sit amet', - labels_bug: { nodes: [] }, - labels: { nodes: [{ title: 'feature' }] }, - }, - ], - }); - }); - - it.each` - fieldName | mockQuery - ${'lastComment'} | ${MOCK_ISSUES_WITH_LAST_COMMENT} - `('extracts data for $fieldName field', ({ fieldName, mockQuery }) => { - const mockData = { project: mockQuery }; - const mockConfig = { - fields: [ - { key: 'title', name: 'title' }, - { - key: 'lastComment', - name: 'lastComment', - transform: derivedFields[fieldName], - }, - ], - }; - - const result = transform(mockData, mockConfig); - - expect(result).toEqual({ - nodes: [ - { - id: '1', - title: 'Lorem ipsum', - }, - { - id: '2', - title: 'Dolor sit amet', - lastComment: 'Hello', - }, - ], - }); - }); - - it('does not iterate over widgets if they do not exist', () => { - const mockData = { project: MOCK_WORK_ITEMS_WITHOUT_WIDGETS }; - const mockConfig = { - fields: [{ key: 'title', name: 'title' }], - }; - - const result = transform(mockData, mockConfig); - - expect(result).toEqual({ - nodes: [ - { id: '1', title: 'Lorem ipsum' }, - { id: '2', title: 'Dolor sit amet' }, - ], - }); - }); - }); -}); diff --git a/spec/frontend/glql/core/transformer/functions_spec.js b/spec/frontend/glql/core/transformer/functions_spec.js deleted file mode 100644 index 3d976c78d1d..00000000000 --- a/spec/frontend/glql/core/transformer/functions_spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import * as functions from '~/glql/core/transformer/functions'; - -describe('GLQL Transformer Functions', () => { - describe('labels', () => { - it('returns correct field name', () => { - const labelsFunction = functions.getFunction('labels'); - expect(labelsFunction.getFieldName('bug', 'feature')).toBe('labels'); - }); - - it('returns correct field label', () => { - const labelsFunction = functions.getFunction('labels'); - expect(labelsFunction.getFieldLabel('bug', 'feature')).toBe('Labels: Bug, Feature'); - }); - - describe('getTransformer', () => { - const getTransformer = (values, mockData) => { - const labelsFunction = functions.getFunction('labels'); - const transformer = labelsFunction.getTransformer('custom_key', ...values); - - return transformer(mockData); - }; - - it('allows 10 values to be provided', () => { - const values = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']; - const mockData = { id: '1', labels: { nodes: [] } }; - - expect(() => getTransformer(values, mockData)).not.toThrow(); - }); - - it('throws an error when more than 10 values are provided', () => { - const values = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']; - const mockData = { id: '1', labels: { nodes: [] } }; - - expect(() => getTransformer(values, mockData)).toThrow( - 'Function `labels` can only take a maximum of 10 parameters.', - ); - }); - - it('transforms data correctly', () => { - const values = ['bug', 'feature*', '*maintenance*']; - const mockIssue1 = { - id: '1', - labels: { - nodes: [{ title: 'bug::closed' }, { title: 'bug' }, { title: 'critical' }], - }, - }; - const mockIssue2 = { - id: '2', - labels: { - nodes: [ - { title: 'feature-request' }, - { title: 'enhancement' }, - { title: 'a-maintenance-b' }, - ], - }, - }; - - expect(getTransformer(values, mockIssue1)).toEqual({ - id: '1', - custom_key: { nodes: [{ title: 'bug' }] }, - labels: { nodes: [{ title: 'bug::closed' }, { title: 'critical' }] }, - }); - - expect(getTransformer(values, mockIssue2)).toEqual({ - id: '2', - custom_key: { nodes: [{ title: 'feature-request' }, { title: 'a-maintenance-b' }] }, - labels: { nodes: [{ title: 'enhancement' }] }, - }); - }); - }); - }); -}); diff --git a/spec/frontend/glql/index_spec.js b/spec/frontend/glql/index_spec.js index 66e4ecf8026..f94c47cf5e8 100644 --- a/spec/frontend/glql/index_spec.js +++ b/spec/frontend/glql/index_spec.js @@ -2,7 +2,7 @@ import renderGlqlNodes from '~/glql'; import { stubCrypto } from 'helpers/crypto'; jest.mock('~/lib/graphql'); -jest.mock('~/glql/core/parser/query'); +jest.mock('~/glql/core/parser'); describe('renderGlqlNodes', () => { stubCrypto(); diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index 97e77f41a4b..d93f692c911 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -442,24 +442,6 @@ describe('text_utility', () => { ); }); - describe('wildcardMatch', () => { - it.each` - pattern | str | result - ${'label'} | ${'label'} | ${true} - ${'label'} | ${'a-label'} | ${false} - ${'*label'} | ${'a-label'} | ${true} - ${'label'} | ${'label-a'} | ${false} - ${'label*'} | ${'label-a'} | ${true} - ${'label*'} | ${'a-label-a'} | ${false} - ${'*label'} | ${'a-label-a'} | ${false} - ${'*label*'} | ${'a-label-a'} | ${true} - ${'l*l'} | ${'label'} | ${true} - ${'!@#$%^&*()-=+/?[]{}'} | ${'!@#$%^&*()-=+/?[]{}'} | ${true} - `('returns expected result', ({ pattern, str, result }) => { - expect(textUtils.wildcardMatch(str, pattern)).toBe(result); - }); - }); - describe('sha256', () => { beforeEach(stubCrypto); diff --git a/spec/frontend/pinia/plugins_spec.js b/spec/frontend/pinia/plugins_spec.js index 28b8ecdf6a8..76e8dbd0f0a 100644 --- a/spec/frontend/pinia/plugins_spec.js +++ b/spec/frontend/pinia/plugins_spec.js @@ -124,6 +124,8 @@ describe('Pinia plugins', () => { createPiniaStore(undefined); setActivePinia(createPinia().use(syncWithVuex)); usePiniaStore().syncWith(createSyncWithConfig()); + // attach twice to test proper unsubscribe + usePiniaStore().syncWith(createSyncWithConfig()); }, ], ])('%s', (caseName, setupFn) => { @@ -138,57 +140,73 @@ describe('Pinia plugins', () => { describe('primitives', () => { it('syncs Pinia with Vuex', () => { + const spy = jest.spyOn(usePiniaStore(), 'setPrimitive'); vuexStore.commit(getVuexName('setPrimitive'), 'newValue'); expect(usePiniaStore().primitive).toBe('newValue'); + expect(spy).toHaveBeenCalledTimes(1); }); it('syncs Vuex with Pinia', () => { + const spy = jest.spyOn(vuexStore, 'commit'); usePiniaStore().setPrimitive('newValue'); expect(getVuexState().primitive).toBe('newValue'); + expect(spy).toHaveBeenCalledTimes(1); }); }); describe('root objects', () => { it('syncs Pinia with Vuex', () => { + const spy = jest.spyOn(usePiniaStore(), 'setObject'); const obj = { foo: 1 }; vuexStore.commit(getVuexName('setObject'), obj); expect(usePiniaStore().object).toStrictEqual(obj); + expect(spy).toHaveBeenCalledTimes(1); }); it('syncs Vuex with Pinia after Pinia is initialized', () => { + const spy = jest.spyOn(vuexStore, 'commit'); usePiniaStore(); const obj = { foo: 1 }; vuexStore.commit(getVuexName('setObject'), obj); expect(usePiniaStore().object).toStrictEqual(obj); + expect(spy).toHaveBeenCalledTimes(1); }); it('syncs Vuex with Pinia', () => { + const spy = jest.spyOn(vuexStore, 'commit'); const obj = { foo: 1 }; usePiniaStore().setObject(obj); expect(getVuexState().object).toStrictEqual(obj); + expect(spy).toHaveBeenCalledTimes(1); }); }); describe('nested objects', () => { it('syncs Pinia with Vuex', async () => { + const spy = jest.spyOn(usePiniaStore(), 'setDeepNested'); const obj = { foo: 1 }; vuexStore.commit(getVuexName('setDeepNested'), obj); await waitForPromises(); expect(usePiniaStore().nested.object).toStrictEqual(obj); + expect(spy).toHaveBeenCalledTimes(1); }); it('syncs Pinia with Vuex after Pinia is initialized', async () => { usePiniaStore(); + const spy = jest.spyOn(usePiniaStore(), 'setDeepNested'); const obj = { foo: 1 }; vuexStore.commit(getVuexName('setDeepNested'), obj); await waitForPromises(); expect(usePiniaStore().nested.object).toStrictEqual(obj); + expect(spy).toHaveBeenCalledTimes(1); }); it('syncs Vuex with Pinia', () => { + const spy = jest.spyOn(vuexStore, 'commit'); const obj = { foo: 1 }; usePiniaStore().setDeepNested(obj); expect(getVuexState().nested.object).toStrictEqual(obj); + expect(spy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/spec/frontend/vue_shared/access_tokens/components/access_token_statistics_spec.js b/spec/frontend/vue_shared/access_tokens/components/access_token_statistics_spec.js index 7ee27929ab1..f68c3804009 100644 --- a/spec/frontend/vue_shared/access_tokens/components/access_token_statistics_spec.js +++ b/spec/frontend/vue_shared/access_tokens/components/access_token_statistics_spec.js @@ -13,10 +13,16 @@ describe('AccessTokenStatistics', () => { const pinia = createTestingPinia(); const store = useAccessTokens(); + const $router = { + push: jest.fn(), + }; const createComponent = () => { wrapper = mountExtended(AccessTokenStatistics, { pinia, + mocks: { + $router, + }, }); }; @@ -47,6 +53,7 @@ describe('AccessTokenStatistics', () => { { type: 'state', value: { data: 'active', operator: '=' } }, ]); expect(store.setPage).toHaveBeenCalledWith(1); + expect($router.push).toHaveBeenCalledWith({ query: { page: 1, sort: 'expires_asc' } }); expect(store.fetchTokens).toHaveBeenCalledTimes(1); }); }); diff --git a/spec/frontend/vue_shared/access_tokens/components/access_tokens_spec.js b/spec/frontend/vue_shared/access_tokens/components/access_tokens_spec.js index 533c644e3f9..eee93ab05b9 100644 --- a/spec/frontend/vue_shared/access_tokens/components/access_tokens_spec.js +++ b/spec/frontend/vue_shared/access_tokens/components/access_tokens_spec.js @@ -17,6 +17,10 @@ describe('AccessTokens', () => { const pinia = createTestingPinia(); const store = useAccessTokens(); + const $router = { + push: jest.fn(), + replace: jest.fn(), + }; const accessTokenCreate = '/api/v4/groups/1/service_accounts/:id/personal_access_tokens/'; const accessTokenRevoke = '/api/v4/groups/2/service_accounts/:id/personal_access_tokens/'; @@ -37,6 +41,9 @@ describe('AccessTokens', () => { id, ...props, }, + mocks: { + $router, + }, }); }; @@ -55,6 +62,7 @@ describe('AccessTokens', () => { createComponent(); waitForPromises(); + expect($router.replace).toHaveBeenCalledWith({ query: { page: 1, sort: 'expires_asc' } }); expect(store.setup).toHaveBeenCalledWith({ filters: DEFAULT_FILTER, id: 235, @@ -126,19 +134,32 @@ describe('AccessTokens', () => { }); }); - it('fetches tokens when the page is changed', () => { - createComponent(); - expect(store.fetchTokens).toHaveBeenCalledTimes(1); - findPagination().vm.$emit('input', 2); - - expect(store.fetchTokens).toHaveBeenCalledTimes(2); - }); - it('fetches tokens when filters are changed', () => { createComponent(); expect(store.fetchTokens).toHaveBeenCalledTimes(1); findFilteredSearch().vm.$emit('submit', ['my token']); + expect($router.push).toHaveBeenCalledWith({ query: { page: 1, sort: 'expires_asc' } }); + expect(store.fetchTokens).toHaveBeenCalledTimes(2); + }); + + it('sets the url params correctly and fetches tokens when window `popstate` event is triggered', () => { + createComponent(); + expect(store.fetchTokens).toHaveBeenCalledTimes(1); + window.dispatchEvent(new Event('popstate')); + + expect(store.setFilters).toHaveBeenCalledTimes(1); + expect(store.setPage).toHaveBeenCalledWith(1); + expect(store.setSorting).toHaveBeenCalledTimes(1); + expect(store.fetchTokens).toHaveBeenCalledTimes(2); + }); + + it('fetches tokens when the page is changed', () => { + createComponent(); + expect(store.fetchTokens).toHaveBeenCalledTimes(1); + findPagination().vm.$emit('input', 2); + + expect($router.push).toHaveBeenCalledWith({ query: { page: 1, sort: 'expires_asc' } }); expect(store.fetchTokens).toHaveBeenCalledTimes(2); }); @@ -147,6 +168,7 @@ describe('AccessTokens', () => { expect(store.fetchTokens).toHaveBeenCalledTimes(1); findSorting().vm.$emit('sortByChange', 'name'); + expect($router.push).toHaveBeenCalledWith({ query: { page: 1, sort: 'expires_asc' } }); expect(store.setSorting).toHaveBeenCalledWith(expect.objectContaining({ value: 'name' })); expect(store.fetchTokens).toHaveBeenCalledTimes(2); }); @@ -157,6 +179,7 @@ describe('AccessTokens', () => { store.sorting = { value: 'name', isAsc: true }; findSorting().vm.$emit('sortDirectionChange', false); + expect($router.push).toHaveBeenCalledWith({ query: { page: 1, sort: 'name_asc' } }); expect(store.setSorting).toHaveBeenCalledWith(expect.objectContaining({ isAsc: false })); expect(store.fetchTokens).toHaveBeenCalledTimes(2); }); diff --git a/spec/frontend/vue_shared/access_tokens/stores/access_tokens_spec.js b/spec/frontend/vue_shared/access_tokens/stores/access_tokens_spec.js index e6eb9249457..86085559602 100644 --- a/spec/frontend/vue_shared/access_tokens/stores/access_tokens_spec.js +++ b/spec/frontend/vue_shared/access_tokens/stores/access_tokens_spec.js @@ -586,6 +586,14 @@ describe('useAccessTokens store', () => { }); describe('getters', () => { + describe('params', () => { + it('returns correct value', () => { + store.page = 2; + + expect(store.params).toEqual({ page: 2 }); + }); + }); + describe('sort', () => { it('returns correct value', () => { expect(store.sort).toBe('expires_asc'); @@ -595,5 +603,17 @@ describe('useAccessTokens store', () => { expect(store.sort).toBe('name_desc'); }); }); + + describe('urlParmas', () => { + it('return correct value', () => { + store.page = 2; + store.sorting = { value: 'name', isAsc: false }; + + expect(store.urlParams).toEqual({ + page: 2, + sort: 'name_desc', + }); + }); + }); }); }); diff --git a/spec/frontend/vue_shared/access_tokens/utils_spec.js b/spec/frontend/vue_shared/access_tokens/utils_spec.js index 260e4612ceb..33c6cf73b93 100644 --- a/spec/frontend/vue_shared/access_tokens/utils_spec.js +++ b/spec/frontend/vue_shared/access_tokens/utils_spec.js @@ -2,14 +2,7 @@ import { defaultDate, serializeParams, update15DaysFromNow, - updateUrlWithQueryParams, } from '~/vue_shared/access_tokens/utils'; -import { getBaseURL, updateHistory } from '~/lib/utils/url_utility'; - -jest.mock('~/lib/utils/url_utility', () => ({ - ...jest.requireActual('~/lib/utils/url_utility'), - updateHistory: jest.fn(), -})); // Current date, `new Date()`, for these tests is 2020-07-06 describe('defaultDate', () => { @@ -97,14 +90,3 @@ describe('update2WeekFromNow', () => { expect(result[0].filters).not.toBe(param[0].filters); }); }); - -describe('updateUrlWithQueryParams', () => { - it('calls updateHistory with correct parameters', () => { - updateUrlWithQueryParams({ params: { page: 1, revoked: 'true' }, sort: 'name_asc' }); - - expect(updateHistory).toHaveBeenCalledWith({ - url: `${getBaseURL()}/?page=1&revoked=true&sort=name_asc`, - replace: true, - }); - }); -}); diff --git a/spec/keeps/delete_old_feature_flags_spec.rb b/spec/keeps/delete_old_feature_flags_spec.rb index 95742412fc8..ddf745a9e40 100644 --- a/spec/keeps/delete_old_feature_flags_spec.rb +++ b/spec/keeps/delete_old_feature_flags_spec.rb @@ -36,6 +36,8 @@ RSpec.describe Keeps::DeleteOldFeatureFlags, feature_category: :tooling do before do stub_request(:get, Keeps::Helpers::Groups::GROUPS_JSON_URL).to_return(status: 200, body: groups.to_json) + stub_request(:get, format(described_class::API_ISSUE_URL, project_path: 'gitlab-org%2Fgitlab', issue_iid: '123')) + .to_return(status: 200, body: { labels: [] }.to_json) allow(keep).to receive(:all_feature_flag_files).and_return([feature_flag_file]) allow(keep).to receive(:milestones_helper).and_return(milestones_helper) @@ -258,6 +260,125 @@ RSpec.describe Keeps::DeleteOldFeatureFlags, feature_category: :tooling do expect(keep.send(:can_remove_ff?, feature_flag, identifiers, :enabled)).to be true end end + + context 'when feature flag has ready for removal label' do + let(:feature_flag) do + instance_double( + Feature::Definition, + name: feature_flag_name, + milestone: nil, # This would normally fail validation + rollout_issue_url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/123', + default_enabled: false, + group: groups.dig(:foo, :label), + path: feature_flag_file, + intended_to_rollout_by: (Time.zone.today + 30).to_s # Future date would normally fail validation + ) + end + + before do + stub_request(:get, + format(described_class::API_ISSUE_URL, project_path: 'gitlab-org%2Fgitlab', issue_iid: '123') + ).to_return(status: 200, body: { labels: ['feature flag::ready for removal'] }.to_json) + + # Make milestone cutoff check fail to prove it's bypassed + allow(milestones_helper) + .to receive(:before_cuttoff?).with(milestone: feature_flag_milestone, + milestones_ago: described_class::CUTOFF_MILESTONE_FOR_ENABLED_FLAG) + .and_return(false) + end + + it 'bypasses rollout date, milestone, and cutoff checks and returns true' do + expect(keep.send(:can_remove_ff?, feature_flag, identifiers, :enabled)).to be true + end + end + + context 'when feature flag does not have ready for removal label' do + before do + stub_request(:get, + format(described_class::API_ISSUE_URL, project_path: 'gitlab-org%2Fgitlab', issue_iid: '123') + ).to_return(status: 200, body: { labels: ['some other label'] }.to_json) + + # Make milestone cutoff check fail + allow(milestones_helper) + .to receive(:before_cuttoff?).with(milestone: feature_flag_milestone, + milestones_ago: described_class::CUTOFF_MILESTONE_FOR_ENABLED_FLAG) + .and_return(false) + end + + it 'respects milestone cutoff check and returns false' do + expect(keep.send(:can_remove_ff?, feature_flag, identifiers, :enabled)).to be false + end + end + end + + describe '#has_ready_for_removal_label?' do + let(:feature_flag) do + instance_double( + Feature::Definition, + name: feature_flag_name, + rollout_issue_url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/123' + ) + end + + before do + allow(keep).to receive(:logger).and_return(double.as_null_object) + allow(keep).to receive(:feature_flag_rollout_issue_url).and_return(feature_flag.rollout_issue_url) + end + + context 'when rollout issue has ready for removal label' do + before do + stub_request(:get, + format(described_class::API_ISSUE_URL, project_path: 'gitlab-org%2Fgitlab', issue_iid: '123') + ).to_return(status: 200, body: { labels: ['feature flag::ready for removal', + 'other label'] }.to_json) + end + + it 'returns true' do + expect(keep.send(:has_ready_for_removal_label?, feature_flag)).to be true + end + end + + context 'when rollout issue does not have ready for removal label' do + before do + stub_request(:get, + format(described_class::API_ISSUE_URL, project_path: 'gitlab-org%2Fgitlab', issue_iid: '123') + ).to_return(status: 200, body: { labels: ['some other label'] }.to_json) + end + + it 'returns false' do + expect(keep.send(:has_ready_for_removal_label?, feature_flag)).to be false + end + end + + context 'when rollout issue URL is missing' do + let(:feature_flag) do + instance_double( + Feature::Definition, + name: feature_flag_name, + rollout_issue_url: nil + ) + end + + before do + allow(keep).to receive(:feature_flag_rollout_issue_url).and_return('(missing URL)') + end + + it 'returns false' do + expect(keep.send(:has_ready_for_removal_label?, feature_flag)).to be false + end + end + + context 'when API request fails' do + before do + stub_request(:get, + format(described_class::API_ISSUE_URL, project_path: 'gitlab-org%2Fgitlab', issue_iid: '123')) + .to_return(status: 404) + end + + it 'returns false' do + expect(keep.send(:has_ready_for_removal_label?, feature_flag)).to be false + end + end end describe '#each_change' do diff --git a/spec/lib/gitlab/diff/line_spec.rb b/spec/lib/gitlab/diff/line_spec.rb index e27f2c6a0fc..bb116b0b086 100644 --- a/spec/lib/gitlab/diff/line_spec.rb +++ b/spec/lib/gitlab/diff/line_spec.rb @@ -153,13 +153,15 @@ RSpec.describe Gitlab::Diff::Line do end describe '#id' do + let(:file_hash) { '1234567890' } + + subject(:id) { line.id(file_hash) } + context 'when meta line' do - it 'returns nil' do - expect(line.id('', :old)).to be_nil - end + it { is_expected.to be_nil } end - context 'when changed line' do + context 'with added line' do let(:line) do described_class.new( '', @@ -173,15 +175,41 @@ RSpec.describe Gitlab::Diff::Line do ) end - let(:file_hash) { '1234567890' } + it { is_expected.to eq("line_#{file_hash[0..8]}_A#{line.new_pos}") } + end - it 'returns the correct old side ID' do - expect(line.id(file_hash, :old)).to eq("line_#{file_hash[0..8]}_L#{line.old_pos}") + context 'with unchanged line' do + let(:line) do + described_class.new( + '', + nil, + 1, + 10, + 11, + parent_file: double(:file), + line_code: double(:line_code), + rich_text: rich_text + ) end - it 'returns the correct new side ID' do - expect(line.id(file_hash, :new)).to eq("line_#{file_hash[0..8]}_R#{line.new_pos}") + it { is_expected.to eq("line_#{file_hash[0..8]}_#{line.old_pos}") } + end + + context 'with removed line' do + let(:line) do + described_class.new( + '', + 'old', + 1, + 10, + 11, + parent_file: double(:file), + line_code: double(:line_code), + rich_text: rich_text + ) end + + it { is_expected.to eq("line_#{file_hash[0..8]}_#{line.old_pos}") } end end end diff --git a/spec/lib/gitlab/topology_service_client/base_service_spec.rb b/spec/lib/gitlab/topology_service_client/base_service_spec.rb index 998696c6a36..163f90745df 100644 --- a/spec/lib/gitlab/topology_service_client/base_service_spec.rb +++ b/spec/lib/gitlab/topology_service_client/base_service_spec.rb @@ -14,4 +14,152 @@ RSpec.describe Gitlab::TopologyServiceClient::BaseService, feature_category: :ce end end end + + describe '#service_credentials' do + let(:ca_content) { 'ca_certificate' } + let(:key_content) { 'private_key' } + let(:cert_content) { 'certificate' } + + let(:ca_file) do + Tempfile.new.tap do |f| + f.write(ca_content) + f.rewind + end + end + + let(:key_file) do + Tempfile.new.tap do |f| + f.write(key_content) + f.rewind + end + end + + let(:cert_file) do + Tempfile.new.tap do |f| + f.write(cert_content) + f.rewind + end + end + + let(:config) do + { + ca_file: ca_file.path, + private_key_file: key_file.path, + certificate_file: cert_file.path + } + end + + subject(:service_credentials) { base_service.send(:service_credentials) } + + before do + stub_config(cell: { enabled: true, topology_service_client: config }) + end + + after do + [key_file, cert_file, ca_file].each(&:close!) + end + + shared_examples 'insecure credentials' do + it 'creates insecure credentials' do + expect(GRPC::Core::ChannelCredentials).to receive(:new).with(no_args) + service_credentials + end + end + + context 'when all certificate files are present' do + it 'creates credentials with the file contents' do + expect(GRPC::Core::ChannelCredentials).to receive(:new).with(ca_content, key_content, cert_content) + service_credentials + end + end + + context 'when ca_file is not present' do + let(:config) do + { + ca_file: nil, + private_key_file: key_file.path, + certificate_file: cert_file.path + } + end + + it 'creates credentials with key and cert only' do + expect(GRPC::Core::ChannelCredentials).to receive(:new).with(nil, key_content, cert_content) + service_credentials + end + end + + context 'with missing private key' do + context 'when private_key_file key is not defined in config' do + let(:config) do + { + ca_file: ca_file.path, + certificate_file: cert_file.path + } + end + + include_examples 'insecure credentials' + end + + context 'when private_key_file value is nil' do + let(:config) do + { + ca_file: ca_file.path, + private_key_file: nil, + certificate_file: cert_file.path + } + end + + include_examples 'insecure credentials' + end + end + + context 'with missing certificate' do + context 'when certificate_file key is not defined in config' do + let(:config) do + { + ca_file: ca_file.path, + private_key_file: key_file.path + } + end + + include_examples 'insecure credentials' + end + + context 'when certificate_file value is nil' do + let(:config) do + { + ca_file: ca_file.path, + private_key_file: key_file.path, + certificate_file: nil + } + end + + include_examples 'insecure credentials' + end + end + + context 'when config values are empty strings' do + let(:config) do + { + ca_file: ca_file.path, + private_key_file: '', + certificate_file: '' + } + end + + include_examples 'insecure credentials' + end + + context 'when certificate files do not exist on filesystem' do + let(:config) do + { + ca_file: ca_file.path, + private_key_file: '/nonexistent/key.pem', + certificate_file: '/nonexistent/cert.pem' + } + end + + include_examples 'insecure credentials' + end + end end diff --git a/spec/migrations/20250624155101_queue_backfill_pipeline_sd_and_cs_analyzer_project_statuses_spec.rb b/spec/migrations/20250624155101_queue_backfill_pipeline_sd_and_cs_analyzer_project_statuses_spec.rb new file mode 100644 index 00000000000..3533a6353c5 --- /dev/null +++ b/spec/migrations/20250624155101_queue_backfill_pipeline_sd_and_cs_analyzer_project_statuses_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueBackfillPipelineSdAndCsAnalyzerProjectStatuses, migration: :gitlab_sec, feature_category: :security_asset_inventories do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + gitlab_schema: :gitlab_sec, + table_name: :analyzer_project_statuses, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6054988918e..c6c2e6f40ba 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1662,6 +1662,38 @@ RSpec.describe User, feature_category: :user_profile do .to contain_exactly(user_with_group, user_with_project) end end + + describe '.with_incoming_email_token' do + let_it_be(:user) { create(:user, incoming_email_token: 'test_token') } + + it 'returns user with matching single token' do + expect(described_class.with_incoming_email_token('test_token')).to contain_exactly(user) + end + + it 'returns user with matching token in array' do + expect(described_class.with_incoming_email_token(%w[test_token other_token])).to contain_exactly(user) + end + + it 'returns empty relation when no match found' do + expect(described_class.with_incoming_email_token('nonexistent')).to be_empty + end + end + + describe '.with_feed_token' do + let_it_be(:user) { create(:user, feed_token: 'test_feed_token') } + + it 'returns user with matching single token' do + expect(described_class.with_feed_token('test_feed_token')).to contain_exactly(user) + end + + it 'returns user with matching token in array' do + expect(described_class.with_feed_token(%w[test_feed_token other_token])).to contain_exactly(user) + end + + it 'returns empty relation when no match found' do + expect(described_class.with_feed_token('nonexistent')).to be_empty + end + end end context 'strip attributes' do diff --git a/spec/policies/container_registry/protection/tag_rule_policy_spec.rb b/spec/policies/container_registry/protection/tag_rule_policy_spec.rb index e3e67f60251..f70a04d0c67 100644 --- a/spec/policies/container_registry/protection/tag_rule_policy_spec.rb +++ b/spec/policies/container_registry/protection/tag_rule_policy_spec.rb @@ -24,25 +24,7 @@ RSpec.describe ContainerRegistry::Protection::TagRulePolicy, feature_category: : it { expect_allowed(:destroy_container_registry_protection_tag_rule) } end - context 'for an immutable tag rule' do - let_it_be(:rule) { build(:container_registry_protection_tag_rule, :immutable, project:) } - - where(:user_role, :expected_result) do - :developer | :be_disallowed - :maintainer | :be_disallowed - :owner | :be_allowed - end - - with_them do - before do - project.send(:"add_#{user_role}", user) - end - - it { is_expected.to send(expected_result, :destroy_container_registry_protection_tag_rule) } - end - end - - context 'for a mutable tag rule' do + context 'for a tag rule' do let_it_be(:rule) { build(:container_registry_protection_tag_rule, project:) } where(:user_role, :expected_result) do diff --git a/spec/policies/container_repository_policy_spec.rb b/spec/policies/container_repository_policy_spec.rb index 08ed946dc45..f3a09ea34a3 100644 --- a/spec/policies/container_repository_policy_spec.rb +++ b/spec/policies/container_repository_policy_spec.rb @@ -24,57 +24,7 @@ RSpec.describe ContainerRepositoryPolicy, feature_category: :container_registry allow(container_repository).to receive(:has_tags?).and_return(has_tags) end - context 'when the project has an immutable tag protection rule' do - before_all do - create( - :container_registry_protection_tag_rule, - :immutable, - project: project - ) - end - - context 'when the container repository has tags' do - let(:has_tags) { true } - - [:owner, :maintainer, :developer].each do |user_role| - context "when the user is #{user_role}" do - before do - project.send(:"add_#{user_role}", user) - end - - it { expect_allowed(:destroy_container_image) } - end - end - - context 'when the current user is an admin', :enable_admin_mode do - let(:user) { build_stubbed(:admin) } - - it { expect_allowed(:destroy_container_image) } - end - end - - context 'when the container repository does not have tags' do - let(:has_tags) { false } - - [:owner, :maintainer, :developer].each do |user_role| - context "when the user is #{user_role}" do - before do - project.send(:"add_#{user_role}", user) - end - - it { expect_allowed(:destroy_container_image) } - end - end - - context 'when the current user is an admin', :enable_admin_mode do - let(:user) { build_stubbed(:admin) } - - it { expect_allowed(:destroy_container_image) } - end - end - end - - context 'when the project has a mutable tag protection rule' do + context 'when the project has a tag protection rule' do before_all do create( :container_registry_protection_tag_rule, diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index efeb8ecbdd1..9f72b32bf1e 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -184,9 +184,19 @@ RSpec.describe 'GraphQL', feature_category: :shared do context 'when batching mutations and queries' do let(:batched) do + single_query = "query A #{graphql_query_for('echo', text: 'Hello world')}" + multipart_query = <<-QUERY + query B #{graphql_query_for('echo', text: 'Hello world')} + mutation C { echoCreate(input: { messages: [\"hello\", \"world\"] }) { echoes } } + QUERY [ - { query: "query A #{graphql_query_for('echo', text: 'Hello world')}" }, - { query: 'mutation B { echoCreate(input: { messages: ["hello", "world"] }) { echoes } }' } + { + query: single_query + }, + { + query: multipart_query, + operationName: "C" + } ] end diff --git a/spec/services/container_registry/protection/update_tag_rule_service_spec.rb b/spec/services/container_registry/protection/update_tag_rule_service_spec.rb index bce819b8636..449c8d160a7 100644 --- a/spec/services/container_registry/protection/update_tag_rule_service_spec.rb +++ b/spec/services/container_registry/protection/update_tag_rule_service_spec.rb @@ -173,12 +173,4 @@ RSpec.describe ContainerRegistry::Protection::UpdateTagRuleService, '#execute', it_behaves_like 'an erroneous service response', message: 'GitLab container registry API not supported' end - - context 'when the rule is immutable' do - let_it_be(:container_protection_tag_rule) do - create(:container_registry_protection_tag_rule, :immutable, project: project, tag_name_pattern: 'a') - end - - it_behaves_like 'an erroneous service response', message: 'Operation not allowed' - end end diff --git a/tooling/graphql/docs/helper.rb b/tooling/graphql/docs/helper.rb index 470981f63c1..303c8b819cb 100644 --- a/tooling/graphql/docs/helper.rb +++ b/tooling/graphql/docs/helper.rb @@ -55,8 +55,8 @@ module Tooling def auto_generated_comment <<-MD.strip_heredoc --- - stage: Foundations - group: Import and Integrate + stage: Create + group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: GraphQL API resources --- diff --git a/workhorse/internal/ai_assist/duoworkflow/client_test.go b/workhorse/internal/ai_assist/duoworkflow/client_test.go index 37efce7560a..7e0ca83b265 100644 --- a/workhorse/internal/ai_assist/duoworkflow/client_test.go +++ b/workhorse/internal/ai_assist/duoworkflow/client_test.go @@ -99,9 +99,6 @@ func TestExecuteWorkflow(t *testing.T) { workflowStream, err := client.ExecuteWorkflow(ctx) require.NoError(t, err) - err = workflowStream.Send([]byte(`{"type":"test_event"}`)) - require.NoError(t, err) - _, err = workflowStream.Recv() require.Error(t, err) require.Equal(t, codes.Internal, status.Code(err)) diff --git a/yarn.lock b/yarn.lock index 829e024d01e..24adb64d4a3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1438,10 +1438,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454" integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q== -"@gitlab/query-language-rust@0.9.2": - version "0.9.2" - resolved "https://registry.yarnpkg.com/@gitlab/query-language-rust/-/query-language-rust-0.9.2.tgz#be157dd8232563738ff03182bdc7cad7e6711d0f" - integrity sha512-3X/9Sm0Mz9W9ltI/etkrFfCBk7f2L2p5hwdB7QW9cths2Q1WxXALfhT9XtzBptdw4/wikfXvfvrViSe7bYIJ+w== +"@gitlab/query-language-rust@0.11.1": + version "0.11.1" + resolved "https://registry.yarnpkg.com/@gitlab/query-language-rust/-/query-language-rust-0.11.1.tgz#8ba31bf1469da1fc2b5c8dc2578bb60725d21e88" + integrity sha512-JlVXPM6dccc2EwoYB8EHo4Z/vjrGVZ//4VTpp5mgWnLvJLW8cEjIKBl0rbFOWVLRIMdlVVtYrQJH1MLugyDhQg== "@gitlab/stylelint-config@6.2.2": version "6.2.2"