Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-03-05 06:17:11 +00:00
parent 1079a0ed1e
commit ffbcfe8ea9
88 changed files with 935 additions and 456 deletions

View File

@ -10,7 +10,6 @@ Migration/BackgroundMigrationRecord:
- 'lib/gitlab/background_migration/backfill_draft_status_on_merge_requests.rb'
- 'lib/gitlab/background_migration/backfill_project_repositories.rb'
- 'lib/gitlab/background_migration/backfill_topics_title.rb'
- 'lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb'
- 'lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb'
- 'lib/gitlab/background_migration/migrate_null_private_profile_to_false.rb'
- 'lib/gitlab/background_migration/populate_latest_pipeline_ids.rb'

View File

@ -11,7 +11,6 @@ Migration/BatchedMigrationBaseClass:
- 'lib/gitlab/background_migration/backfill_snippet_repositories.rb'
- 'lib/gitlab/background_migration/backfill_topics_title.rb'
- 'lib/gitlab/background_migration/create_security_setting.rb'
- 'lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb'
- 'lib/gitlab/background_migration/fix_projects_without_project_feature.rb'
- 'lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb'
- 'lib/gitlab/background_migration/legacy_upload_mover.rb'

View File

@ -2,6 +2,8 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
import { stringifyTime, parseSeconds } from '~/lib/utils/datetime/date_format_utility';
import { localeDateFormat } from '~/lib/utils/datetime/locale_dateformat';
export default {
components: {
@ -35,6 +37,17 @@ export default {
return durationSeries;
},
},
methods: {
formatDate(isoDateStr) {
if (isoDateStr) {
return localeDateFormat.asDate.format(new Date(isoDateStr));
}
return '';
},
formatDuration(seconds) {
return stringifyTime(parseSeconds(seconds, { daysPerWeek: 7, hoursPerDay: 24 }));
},
},
lineChartOptions: {
yAxis: {
name: s__('Pipeline|Seconds'),
@ -55,6 +68,13 @@ export default {
:data="data"
:option="$options.lineChartOptions"
:include-legend-avg-max="false"
/>
>
<template #tooltip-title="{ params }">
<template v-if="params && params.value">{{ formatDate(params.value) }}</template>
</template>
<template #tooltip-value="{ value }">
{{ formatDuration(value) }}
</template>
</gl-line-chart>
</div>
</template>

View File

@ -7,6 +7,7 @@ import {
DATA_VIZ_BLUE_500,
} from '@gitlab/ui/src/tokens/build/js/tokens';
import { s__ } from '~/locale';
import { localeDateFormat } from '~/lib/utils/datetime/locale_dateformat';
export default {
components: {
@ -51,6 +52,14 @@ export default {
return this.data.bars;
},
},
methods: {
formatDate(isoDateStr) {
if (isoDateStr) {
return localeDateFormat.asDate.format(new Date(isoDateStr));
}
return '';
},
},
palette: [DATA_VIZ_GREEN_500, DATA_VIZ_MAGENTA_600, DATA_VIZ_BLUE_500],
};
</script>
@ -68,6 +77,10 @@ export default {
:group-by="groupBy"
:bars="bars"
:include-legend-avg-max="false"
/>
>
<template #tooltip-title="{ params }">
<template v-if="params">{{ formatDate(params.value) }}</template>
</template>
</gl-stacked-column-chart>
</div>
</template>

View File

@ -2,7 +2,6 @@
import { GlAlert, GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, s__, sprintf } from '~/locale';
import autopopulateAllowlistMutation from '../graphql/mutations/autopopulate_allowlist.mutation.graphql';
export default {
name: 'AutopopulateAllowlistModal',
@ -33,12 +32,6 @@ export default {
},
},
apollo: {},
data() {
return {
errorMessage: false,
isAutopopulating: false,
};
},
computed: {
authLogExceedsLimitMessage() {
return sprintf(
@ -56,14 +49,12 @@ export default {
text: __('Add entries'),
attributes: {
variant: 'confirm',
loading: this.isAutopopulating,
},
},
actionSecondary: {
text: __('Cancel'),
attributes: {
variant: 'default',
disabled: this.isAutopopulating,
},
},
};
@ -77,48 +68,15 @@ export default {
},
},
methods: {
async autopopulateAllowlist() {
this.isAutopopulating = true;
this.errorMessage = null;
try {
const {
data: {
ciJobTokenScopeAutopopulateAllowlist: { errors },
},
} = await this.$apollo.mutate({
mutation: autopopulateAllowlistMutation,
variables: {
projectPath: this.fullPath,
},
});
if (errors.length) {
throw new Error(errors[0]);
}
this.$emit('refetch-allowlist');
this.hideModal();
this.$toast.show(
s__('CICD|Authentication log entries were successfully added to the allowlist.'),
);
} catch (error) {
this.errorMessage =
error?.message ||
s__(
'CICD|An error occurred while adding the authentication log entries. Please try again.',
);
} finally {
this.isAutopopulating = false;
}
autopopulateAllowlist() {
this.$emit('autopopulate-allowlist');
},
hideModal() {
this.errorMessage = null;
this.$emit('hide');
},
},
compactionAlgorithmHelpPage: helpPagePath('ci/jobs/ci_job_token', {
anchor: 'auto-populate-a-projects-allowlist',
anchor: 'allowlist-compaction',
}),
};
</script>
@ -135,9 +93,6 @@ export default {
@canceled="hideModal"
@hidden="hideModal"
>
<gl-alert v-if="errorMessage" variant="danger" class="gl-mb-3" :dismissible="false">
{{ errorMessage }}
</gl-alert>
<div v-if="authLogExceedsLimit">
<gl-alert variant="warning" class="gl-mb-3" :dismissible="false">
{{ authLogExceedsLimitMessage }}
@ -163,13 +118,16 @@ export default {
<gl-sprintf
:message="
s__(
`CICD|You're about to add all entries from the authentication log to the allowlist for %{projectName}. Duplicate entries will be ignored.`,
`CICD|You're about to add all entries from the authentication log to the allowlist for %{projectName}. This will also update the Job Token setting to %{codeStart}This project and any groups and projects in the allowlist%{codeEnd}, if not already set. Duplicate entries will be ignored.`,
)
"
>
<template #projectName>
<b>{{ projectName }}</b>
</template>
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
<p>
@ -182,7 +140,7 @@ export default {
<p>
{{
s__(
'CICD|The process to add entries could take a moment to complete with large logs or allowlists.',
'CICD|The process might take a moment to complete for large authentication logs or allowlists.',
)
}}
</p>

View File

@ -23,6 +23,7 @@ import inboundRemoveGroupCIJobTokenScopeMutation from '../graphql/mutations/inbo
import inboundUpdateCIJobTokenScopeMutation from '../graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql';
import inboundGetCIJobTokenScopeQuery from '../graphql/queries/inbound_get_ci_job_token_scope.query.graphql';
import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '../graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql';
import autopopulateAllowlistMutation from '../graphql/mutations/autopopulate_allowlist.mutation.graphql';
import getCiJobTokenScopeAllowlistQuery from '../graphql/queries/get_ci_job_token_scope_allowlist.query.graphql';
import getAuthLogCountQuery from '../graphql/queries/get_auth_log_count.query.graphql';
import removeAutopopulatedEntriesMutation from '../graphql/mutations/remove_autopopulated_entries.mutation.graphql';
@ -71,16 +72,6 @@ export default {
text: s__('CICD|Only this project and any groups and projects in the allowlist'),
},
],
crudFormActions: [
{
text: __('Group or project'),
value: JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT,
},
{
text: __('All projects in authentication log'),
value: JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG,
},
],
components: {
AutopopulateAllowlistModal,
GlAlert,
@ -206,6 +197,23 @@ export default {
anchor: 'control-job-token-access-to-your-project',
});
},
crudFormActions() {
const actions = [
{
text: __('Group or project'),
value: JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT,
},
];
if (this.authLogCount > 0) {
actions.push({
text: __('All projects in authentication log'),
value: JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG,
});
}
return actions;
},
allowlist() {
const { groups, projects } = this.groupsAndProjectsWithAccess;
return [...groups, ...projects];
@ -224,6 +232,9 @@ export default {
},
];
},
hasAutoPopulatedEntries() {
return this.allowlist.filter((entry) => entry.autopopulated).length > 0;
},
groupCount() {
return this.groupsAndProjectsWithAccess.groups.length;
},
@ -317,6 +328,43 @@ export default {
this.refetchGroupsAndProjects();
return Promise.resolve();
},
async autopopulateAllowlist() {
this.hideSelectedAction();
this.autopopulationErrorMessage = null;
this.allowlistLoadingMessage = s__(
'CICD|Auto-populating allowlist entries. Please wait while the action completes.',
);
try {
const {
data: {
ciJobTokenScopeAutopopulateAllowlist: { errors },
},
} = await this.$apollo.mutate({
mutation: autopopulateAllowlistMutation,
variables: {
projectPath: this.fullPath,
},
});
if (errors.length) {
this.autopopulationErrorMessage = errors[0].message;
return;
}
this.$apollo.queries.inboundJobTokenScopeEnabled.refetch();
this.refetchAllowlist();
this.$toast.show(
s__('CICD|Authentication log entries were successfully added to the allowlist.'),
);
} catch {
this.autopopulationErrorMessage = s__(
'CICD|An error occurred while adding the authentication log entries. Please try again.',
);
} finally {
this.allowlistLoadingMessage = '';
}
},
async removeAutopopulatedEntries() {
this.hideSelectedAction();
this.autopopulationErrorMessage = null;
@ -388,7 +436,7 @@ export default {
:project-name="projectName"
:show-modal="showAutopopulateModal"
@hide="hideSelectedAction"
@refetch-allowlist="refetchAllowlist"
@autopopulate-allowlist="autopopulateAllowlist"
/>
<remove-autopopulated-entries-modal
:show-modal="showRemoveAutopopulatedEntriesModal"
@ -451,13 +499,14 @@ export default {
<template v-if="canAutopopulateAuthLog" #actions="{ showForm }">
<gl-collapsible-listbox
v-model="selectedAction"
:items="$options.crudFormActions"
:items="crudFormActions"
:toggle-text="$options.i18n.add"
data-testid="form-selector"
size="small"
@select="selectAction($event, showForm)"
/>
<gl-disclosure-dropdown
v-if="hasAutoPopulatedEntries"
category="tertiary"
icon="ellipsis_v"
no-caret

View File

@ -302,8 +302,8 @@ export default {
cancelToken: this.refCancelToken.token,
})
.then(({ data }) => {
const branches = data.Branches;
const tags = data.Tags;
const branches = data?.Branches || [];
const tags = data?.Tags || [];
if (target === WORK_ITEM_CREATE_ENTITY_MODAL_TARGET_SOURCE) {
this.invalidSource = !(

View File

@ -135,6 +135,7 @@ class GraphqlController < ApplicationController
private
def check_dpop!
return unless !!sessionless_user? # DPoP is only enforced on token-based authentication
return unless current_user && Feature.enabled?(:dpop_authentication, current_user)
token = extract_personal_access_token

View File

@ -81,6 +81,15 @@ module Ci
})
end
def admin_runners_fleet_dashboard_data
{
admin_runners_path: admin_runners_path,
new_runner_path: new_admin_runner_path,
clickhouse_ci_analytics_available: ::Gitlab::ClickHouse.configured?.to_s,
can_admin_runners: current_user.can_admin_all_resources?.to_s
}
end
def group_shared_runners_settings_data(group)
data = {
group_id: group.id,

View File

@ -11,6 +11,7 @@ module Gitlab
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
deduplicate :until_executed
feature_category :importers
data_consistency :sticky
idempotent!

View File

@ -558,7 +558,7 @@ Settings.cron_jobs['prune_old_events_worker'] ||= {}
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
Settings.cron_jobs['gitlab_export_prune_project_export_jobs_worker'] ||= {}
Settings.cron_jobs['gitlab_export_prune_project_export_jobs_worker']['cron'] ||= '30 3 * * *'
Settings.cron_jobs['gitlab_export_prune_project_export_jobs_worker']['cron'] ||= '30 * * * *'
Settings.cron_jobs['gitlab_export_prune_project_export_jobs_worker']['job_class'] = 'Gitlab::Export::PruneProjectExportJobsWorker'
Settings.cron_jobs['trending_projects_worker'] ||= {}
Settings.cron_jobs['trending_projects_worker']['cron'] = '0 1 * * *'

View File

@ -20,3 +20,4 @@
Because it scans your project in more detail, Advanced SAST may take more time to scan your project.
If needed, you can [disable GitLab Advanced SAST](https://docs.gitlab.com/user/application_security/sast/gitlab_advanced_sast#disable-gitlab-advanced-sast-scanning) by setting the CI/CD variable `GITLAB_ADVANCED_SAST_ENABLED` to `false`.
You can set this variable in your project, group, or policy now to prevent Advanced SAST from being enabled by default in GitLab 18.0.

View File

@ -163,6 +163,12 @@ it [compacts the allowlist](#allowlist-compaction) to stay under the 200 entry l
#### With the UI
{{< history >}}
- Introduced in [GitLab 17.10](https://gitlab.com/gitlab-org/gitlab/-/issues/498125). [Deployed behind the `:authentication_logs_migration_for_allowlist` feature flag](../../user/feature_flags.md), disabled by default.
{{< /history >}}
To auto-populate the allowlist through the UI:
1. On the left sidebar, select **Search or go** to and find your project.

View File

@ -1,7 +1,7 @@
---
stage: Foundations
group: Global Search
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Advanced search development guidelines
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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 a foreign key constraint to an existing column
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Adding Database Indexes
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Avoiding downtime in migrations
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Batching best practices
---

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Introduction to ClickHouse use and table design
---

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: ClickHouse within GitLab
---

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Store GitLab activity data in ClickHouse
---

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Merge request analytics with ClickHouse
---

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Optimizing query execution
---

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Tiered Storages in ClickHouse
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Database Dictionary
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Database Lab access using the `pgai` Ruby gem
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Database Reviewer Guidelines
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Importing a database dump into a staging environment
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Delete existing migrations
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Efficient `IN` operator queries
---

View File

@ -1,7 +1,7 @@
---
stage: Plan
group: Project Management
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Filtering by label
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Foreign keys and associations
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Keyset pagination
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Database load balancing
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Post Deployment Migrations
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Rename table without downtime
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Swapping Tables
---

View File

@ -1,7 +1,7 @@
---
stage: Data Access
group: Database Frameworks
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Verifying Database Capabilities
---

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Development seed files
---

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Guidelines for implementing Enterprise Edition features
---

View File

@ -1,7 +1,7 @@
---
stage: Plan
group: Project Management
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Issuable-like Rails models utilities
---

View File

@ -11,4 +11,4 @@ This document was moved to [another location](../../editor_extensions/language_s
<!-- This redirect file can be deleted after <2025-02-11>. -->
<!-- Redirects that point to other docs in the same project expire in three months. -->
<!-- Redirects that point to docs in a different project or site (link is not relative and starts with `https:`) expire in one year. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
<!-- Before deletion, see: https://docs.gitlab.com/development/documentation/redirects/ -->

View File

@ -1,7 +1,7 @@
---
stage: Create
group: Code Review
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Developer information explaining terminology and features used in merge requests.
title: Merge request concepts
---

View File

@ -1,7 +1,7 @@
---
stage: Create
group: Code Review
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Developer documentation explaining the design and workflow of merge request approval rules.
title: Approval rules development guidelines
---

View File

@ -1,7 +1,7 @@
---
stage: Create
group: Code Review
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Developer documentation for how diffs are generated and rendered in GitLab.
title: Working with diffs
---

View File

@ -1,7 +1,7 @@
---
stage: Create
group: Code Review
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Developer documentation for the backend design and flow of merge request diffs.
title: Merge request diffs development guide
---

View File

@ -1,7 +1,7 @@
---
stage: Create
group: Code Review
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Developer documentation explaining how the different parts of the Vue-based frontend diffs are generated.
title: Merge request diffs frontend overview
---

View File

@ -1,7 +1,7 @@
---
stage: Create
group: Code Review
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Developer information explaining the process to add a new mergeability check
title: Mergeability framework
---

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Merge Request Performance Guidelines
---

View File

@ -1,7 +1,7 @@
---
stage: Create
group: Source Code
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Application and rate limit guidelines
---

View File

@ -1,7 +1,7 @@
---
stage: Growth
group: Acquisition
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: Product Qualified Lead (PQL) development guidelines
---

View File

@ -1,7 +1,7 @@
---
stage: none
group: unassigned
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
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: RuboCop rule development guidelines
---

View File

@ -1007,6 +1007,7 @@ An automated process migrates results from previous scanners after the first sca
Because it scans your project in more detail, Advanced SAST may take more time to scan your project.
If needed, you can [disable GitLab Advanced SAST](https://docs.gitlab.com/user/application_security/sast/gitlab_advanced_sast#disable-gitlab-advanced-sast-scanning) by setting the CI/CD variable `GITLAB_ADVANCED_SAST_ENABLED` to `false`.
You can set this variable in your project, group, or policy now to prevent Advanced SAST from being enabled by default in GitLab 18.0.
</div>

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -1169,7 +1169,7 @@ Do not change it back to a markdown codeblocks.
-->
<!--
DO NOT change the name of markdown_logo.png. This file is used for a test in
DO NOT change the name of markdown_logo_v17_11.png. This file is used for a test in
spec/controllers/help_controller_spec.rb.
-->
@ -1182,18 +1182,18 @@ Inline-style:
<!-- markdownlint-disable proper-names -->
<pre class="highlight"><code>![alt text](img/markdown_logo.png "Title Text")
<pre class="highlight"><code>![alt text](img/markdown_logo_v17_11.png "Title Text")
</code></pre>
![alt text](img/markdown_logo.png "Title Text")
![alt text](img/markdown_logo_v17_11.png "Title Text")
Reference-style:
<pre class="highlight"><code>![alt text1][logo]
&#91;logo]: img/markdown_logo.png "Title Text"
&#91;logo]: img/markdown_logo_v17_11.png "Title Text"
</code></pre>
![alt text](img/markdown_logo.png "Title Text")
![alt text](img/markdown_logo_v17_11.png "Title Text")
<!-- markdownlint-enable proper-names -->
@ -1230,12 +1230,12 @@ The value must an integer with a unit of either `px` (default) or `%`.
For example
```markdown
![alt text](img/markdown_logo.png "Title Text"){width=100 height=100px}
![alt text](img/markdown_logo_v17_11.png "Title Text"){width=100 height=100px}
![alt text](img/markdown_logo.png "Title Text"){width=75%}
![alt text](img/markdown_logo_v17_11.png "Title Text"){width=75%}
```
![alt text](img/markdown_logo.png "Title Text"){width=100 height=100px}
![alt text](img/markdown_logo_v17_11.png "Title Text"){width=100 height=100px}
You can also use the `img` HTML tag instead of Markdown and set its `height` and
`width` parameters.

View File

@ -17,13 +17,18 @@ rspec:
- name: postgres:${POSTGRES_VERSION}
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
before_script:
- apt update && apt install -y postgresql-client
- psql -h postgres -U $POSTGRES_USER -c 'create database gitlabhq_test;'
- psql -h postgres -U $POSTGRES_USER -c 'create database gitlabhq_ci_test;'
- cp gems/gitlab-backup-cli/spec/fixtures/config/database.yml config/
- "sed -i \"s/username: postgres$/username: $POSTGRES_USER/g\" config/database.yml"
- "sed -i \"s/password:\\s*$/password: $POSTGRES_PASSWORD/g\" config/database.yml"
- "sed -i \"s/host: localhost$/host: postgres/g\" config/database.yml"
- apt update && apt install -y postgresql-client
- psql -h postgres -U $POSTGRES_USER -c 'create database gitlabhq_test;'
- psql -h postgres -U $POSTGRES_USER -c 'create database gitlabhq_ci_test;'
- |
cd gems/gitlab-backup-cli/spec/fixtures/gitlab_fake &&
[ -n "$BUNDLE_GEMFILE" ] && mv Gemfile ${BUNDLE_GEMFILE} && mv Gemfile.lock ${BUNDLE_GEMFILE}.lock
- bundle install --retry=3
- cd -
- !reference [.default, before_script]
script:
- RAILS_ENV=test bundle exec rspec

View File

@ -11,3 +11,6 @@ Rails/Exit:
RSpec/MultipleMemoizedHelpers:
Max: 25
AllowSubject: true
Rails/RakeEnvironment:
Enabled: false

View File

@ -10,3 +10,7 @@ require "rubocop/rake_task"
RuboCop::RakeTask.new
task default: %i[spec rubocop]
task :version do |_|
puts Gitlab::Backup::Cli::VERSION
end

View File

@ -5,6 +5,7 @@ module Gitlab
module Cli
module Errors
autoload :DatabaseBackupError, 'gitlab/backup/cli/errors/database_backup_error'
autoload :DatabaseCleanupError, 'gitlab/backup/cli/errors/database_cleanup_error'
autoload :DatabaseConfigMissingError, 'gitlab/backup/cli/errors/database_config_missing_error'
autoload :DatabaseMissingConnectionError, 'gitlab/backup/cli/errors/database_missing_connection_error'
autoload :FileBackupError, 'gitlab/backup/cli/errors/file_backup_error'

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
module Gitlab
module Backup
module Cli
module Errors
class DatabaseCleanupError < StandardError
attr_reader :task, :path, :error
def initialize(task:, path:, error:)
@task = task
@path = path
@error = error
super(build_message)
end
private
def build_message
"Failed to cleanup GitLab databases \n" \
"Running the following rake task: '#{task}' (from: #{path}) failed:\n" \
"#{error}"
end
end
end
end
end
end

View File

@ -18,6 +18,10 @@ module Gitlab
].freeze
IGNORED_ERRORS_REGEXP = Regexp.union(IGNORED_ERRORS).freeze
# Rake task used to drop all tables from GitLab databases
# This task is executed before restoring data
DROP_TABLES_TASK = "gitlab:db:drop_tables"
attr_reader :errors
def initialize(context)
@ -66,6 +70,10 @@ module Gitlab
def restore(source)
databases = Gitlab::Backup::Cli::Services::Postgres.new(context)
# Drop all tables Load the schema to ensure we don't have any newer tables
# hanging out from a failed upgrade
drop_tables!
databases.each do |db|
database_name = db.configuration.name
pg_database_name = db.configuration.database
@ -89,10 +97,6 @@ module Gitlab
next
end
# Drop all tables Load the schema to ensure we don't have any newer tables
# hanging out from a failed upgrade
drop_tables(db)
Gitlab::Backup::Cli::Output.info "Restoring PostgreSQL database #{pg_database_name} ... "
status = restore_tables(database: db, filepath: db_file_name)
@ -151,18 +155,22 @@ module Gitlab
Gitlab::Backup::Cli::Output.print_tag(status ? :success : :failure)
end
def drop_tables(database)
pg_database_name = database.configuration.database
Gitlab::Backup::Cli::Output.print_info "Cleaning the '#{pg_database_name}' database ... "
def drop_tables!
Gitlab::Backup::Cli::Output.print_info "Cleaning existing databases ... "
if Rake::Task.task_defined? "gitlab:db:drop_tables:#{database.configuration.name}"
Rake::Task["gitlab:db:drop_tables:#{database.configuration.name}"].invoke
else
# In single database (single or two connections)
Rake::Task["gitlab:db:drop_tables"].invoke
gitlab_path = context.gitlab_basepath
# Drop existing tables from configured databases before restoring from a backup
rake = Utils::Rake.new(DROP_TABLES_TASK, chdir: gitlab_path).execute
unless rake.success?
Gitlab::Backup::Cli::Output.print_tag(:failure)
raise Errors::DatabaseCleanupError.new(task: DROP_TABLES_TASK, path: gitlab_path, error: rake.stderr)
end
Gitlab::Backup::Cli::Output.print_tag(:success)
Gitlab::Backup::Cli::Output.info(rake.output) unless rake.output.empty?
end
def restore_tables(database:, filepath:)

View File

@ -6,6 +6,7 @@ module Gitlab
module Utils
autoload :Compression, 'gitlab/backup/cli/utils/compression'
autoload :PgDump, 'gitlab/backup/cli/utils/pg_dump'
autoload :Rake, 'gitlab/backup/cli/utils/rake'
autoload :Tar, 'gitlab/backup/cli/utils/tar'
end
end

View File

@ -0,0 +1,70 @@
# frozen_string_literal: true
module Gitlab
module Backup
module Cli
module Utils
class Rake
# @return [Array<String>] a list of tasks to be executed
attr_reader :tasks
# @return [String|Pathname] a path where rake tasks are run from
attr_reader :chdir
# @param [Array<String>] *tasks a list of tasks to be executed
# @param [String|Pathname] chdir a path where rake tasks are run from
def initialize(*tasks, chdir: Gitlab::Backup::Cli.root)
@tasks = tasks
@chdir = chdir
end
# @return [self]
def execute
Bundler.with_original_env do
@result = Shell::Command.new(*rake_command, chdir: chdir).capture
end
self
end
# Return whether the execution was a success or not
#
# @return [Boolean] whether the execution was a success
def success?
@result&.status&.success? || false
end
# Return the captured rake output
#
# @return [String] stdout content
def output
@result&.stdout || ''
end
# Return the captured error content
#
# @return [String] stdout content
def stderr
@result&.stderr || ''
end
# Return the captured execution duration
#
# @return [Float] execution duration
def duration
@result&.duration || 0.0
end
private
# Return a list of commands necessary to execute `rake`
#
# @return [Array<String (frozen)>] array of commands to be used by Shellout
def rake_command
%w[bundle exec rake] + tasks
end
end
end
end
end
end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
source "https://rubygems.org"
gem 'rake', '~> 13.0'

View File

@ -0,0 +1,18 @@
GEM
remote: https://rubygems.org/
specs:
rake (13.2.1)
PLATFORMS
aarch64-linux
arm64-darwin
ruby
x86-linux
x86_64-darwin
x86_64-linux
DEPENDENCIES
rake (~> 13.0)
BUNDLED WITH
2.5.22

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
namespace :gitlab do
namespace :db do
task :drop_tables do |_|
exit 0
end
end
end
task :current_pwd do |_|
puts Dir.getwd
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
RSpec.describe Gitlab::Backup::Cli::Errors::DatabaseCleanupError do
let(:task) { 'gitlab:task' }
let(:path) { fixtures_path }
let(:error) { 'error message from task execution' }
subject(:database_error) { described_class.new(task: task, path: path, error: error) }
describe '#initialize' do
it 'sets task, path and error attributes' do
expect(database_error.path).to eq(path)
expect(database_error.task).to eq(task)
expect(database_error.error).to eq(error)
end
end
end

View File

@ -1,16 +1,20 @@
# frozen_string_literal: true
RSpec.describe Gitlab::Backup::Cli::Services::Database do
let(:database_yml) { YAML.load_file(fixtures_path.join('config/database.yml'), aliases: true) }
let(:context) { build_test_context }
let(:connection) { database.send(:connection) }
let(:mocked_configuration) do
database_yml = YAML.load_file(fixtures_path.join('config/database.yml'), aliases: true)
ActiveRecord::DatabaseConfigurations.new(database_yml).configs_for(env_name: 'test', include_hidden: false).first
end
let(:test_configuration) do
Gitlab::Backup::Cli::Services::Postgres.new(build_test_context).send(:database_configurations).first
Gitlab::Backup::Cli::Services::Postgres.new(context).send(:database_configurations).first
end
let(:connection) { database.send(:connection) }
after do
context.cleanup!
end
context 'with mocked configuration' do
subject(:database) { described_class.new(mocked_configuration) }

View File

@ -5,6 +5,10 @@ RSpec.describe Gitlab::Backup::Cli::Services::Postgres do
subject(:postgres) { described_class.new(context) }
after do
context.cleanup!
end
describe '#entries' do
context 'with missing database configuration' do
it 'raises an error' do

View File

@ -7,6 +7,10 @@ RSpec.describe Gitlab::Backup::Cli::Targets::Database do
let(:database) { described_class.new(context) }
let(:pipeline_success) { instance_double(Gitlab::Backup::Cli::Shell::Pipeline::Result, success?: true) }
after do
context.cleanup!
end
describe '#dump', :silence_output do
let(:destination) { Pathname(Dir.mktmpdir('database-target', temp_path)) }
@ -17,7 +21,7 @@ RSpec.describe Gitlab::Backup::Cli::Targets::Database do
it 'creates the destination directory' do
mock_database_dump!
expect(FileUtils).to receive(:mkdir_p).with(destination)
expect(destination).to be_directory
database.dump(destination)
end
@ -99,18 +103,16 @@ RSpec.describe Gitlab::Backup::Cli::Targets::Database do
pipeline_success
)
mock_databases_collection('main') do |db|
mock_databases_collection('main') do |_|
FileUtils.touch(source.join('database.sql.gz'))
expect(database).to receive(:drop_tables).with(db)
end
expect(database).to receive(:drop_tables!)
database.restore(source)
end
it 'restores the database' do
allow(database).to receive(:drop_tables)
mock_databases_collection('main') do |db|
filepath = source.join('database.sql.gz')
FileUtils.touch(filepath)

View File

@ -0,0 +1,119 @@
# frozen_string_literal: true
RSpec.describe Gitlab::Backup::Cli::Utils::Rake do
subject(:rake) { described_class.new('version') }
describe '#execute' do
it 'clears out bundler environment' do
expect(Bundler).to receive(:with_original_env).and_yield
rake.execute
end
it 'runs rake using bundle exec' do
expect_next_instance_of(Gitlab::Backup::Cli::Shell::Command) do |shell|
expect(shell.cmd_args).to start_with(%w[bundle exec rake])
end
rake.execute
end
it 'runs rake command with the defined tasks' do
expect_next_instance_of(Gitlab::Backup::Cli::Shell::Command) do |shell|
expect(shell.cmd_args).to end_with(%w[version])
end
rake.execute
expect(rake.success?).to eq(true)
end
context 'when chdir is set' do
let(:tmpdir) { Dir.mktmpdir }
after do
FileUtils.rm_rf(tmpdir)
end
subject(:rake) { described_class.new('current_pwd', chdir: tmpdir) }
it 'runs rake in the provided chdir directory' do
expect_next_instance_of(Gitlab::Backup::Cli::Shell::Command) do |shell|
expect(shell.chdir).to eq(tmpdir)
end
FileUtils.cp_r(fixtures_path.join('gitlab_fake').glob('*'), tmpdir)
rake.execute
expect(rake.success?).to eq(true)
expect(rake.output).to match(/#{tmpdir}/)
end
end
end
describe '#success?' do
subject(:rake) { described_class.new('--version') } # valid command that has no side-effect
context 'with a successful rake execution' do
it 'returns true' do
rake.execute
expect(rake.success?).to be_truthy
end
end
context 'with a failed rake execution', :hide_output do
subject(:invalid_rake) { described_class.new('--invalid') } # valid command that has no side-effect
it 'returns false when a previous execution failed' do
invalid_rake.execute
expect(invalid_rake.duration).to be > 0.0
expect(invalid_rake.success?).to be_falsey
end
end
it 'returns false when no execution was done before' do
expect(rake.success?).to be_falsey
end
end
describe '#output' do
it 'returns the output from running a rake task' do
rake.execute
expect(rake.output).to match(Gitlab::Backup::Cli::VERSION)
end
it 'returns an empty string when the task has not been run' do
expect(rake.output).to eq('')
end
end
describe '#stderr' do
subject(:invalid_rake) { described_class.new('--invalid') } # valid command that has no side-effect
it 'returns the content from stderr when available' do
invalid_rake.execute
expect(invalid_rake.stderr).to match('invalid option: --invalid')
end
it 'returns an empty string when the task has not been run' do
expect(invalid_rake.stderr).to eq('')
end
end
describe '#duration' do
it 'returns a duration time' do
rake.execute
expect(rake.duration).to be > 0.0
end
it 'returns 0.0 when the task has not been run' do
expect(rake.duration).to eq(0.0)
end
end
end

View File

@ -34,7 +34,16 @@ module GitlabBackupHelpers
end
def build_test_context
TestContext.new
TestContext.new.tap do |context|
# config/database.yml
db = context.gitlab_original_basepath.join('config/database.yml')
test_db = context.gitlab_basepath.join('config/database.yml')
FileUtils.mkdir_p(File.dirname(test_db))
FileUtils.copy(db, test_db)
# Mocked Rakefile and Gemfile
FileUtils.cp_r(fixtures_path.join('gitlab_fake').glob('*'), context.gitlab_basepath)
end
end
end

View File

@ -2,11 +2,22 @@
class TestContext < Gitlab::Backup::Cli::Context::SourceContext
def gitlab_basepath
test_helpers.spec_path.join('../../..')
@gitlab_basepath ||= Pathname(Dir.mktmpdir('gitlab', test_helpers.temp_path))
end
def backup_basedir
test_helpers.temp_path.join('backups')
gitlab_basepath.join('backups')
end
def gitlab_original_basepath
test_helpers.spec_path.join('../../..')
end
# Deletes the temporary folders
def cleanup!
dir_permissions = (File.stat(gitlab_basepath).mode & 0o777).to_s(8) # retrieve permissions in octal format)
FileUtils.rm_rf(gitlab_basepath) if dir_permissions == "700" # ensure it's a temporary dir before deleting
end
private

View File

@ -1,97 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Class that fixes the incorrectly set authored_date within
# issue_metrics table
class FixFirstMentionedInCommitAt
SUB_BATCH_SIZE = 500
class TmpIssueMetrics < ActiveRecord::Base
include EachBatch
self.table_name = 'issue_metrics'
def self.from_2020
where(first_mentioned_in_commit_at_condition)
end
def self.first_mentioned_in_commit_at_condition
if columns_hash['first_mentioned_in_commit_at'].sql_type == 'timestamp without time zone'
'EXTRACT(YEAR FROM first_mentioned_in_commit_at) > 2019'
else
"EXTRACT(YEAR FROM first_mentioned_in_commit_at at time zone 'UTC') > 2019"
end
end
end
def perform(start_id, end_id)
scope(start_id, end_id).each_batch(of: SUB_BATCH_SIZE, column: :issue_id) do |sub_batch|
first, last = sub_batch.pick(Arel.sql('min(issue_id), max(issue_id)'))
# The query need to be reconstructed because .each_batch modifies the default scope
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/330510
inner_query = TmpIssueMetrics
.unscoped
.merge(scope(first, last))
.from("issue_metrics, #{lateral_query}")
.select('issue_metrics.issue_id', 'first_authored_date.authored_date')
.where('issue_metrics.first_mentioned_in_commit_at > first_authored_date.authored_date')
TmpIssueMetrics.connection.execute <<~UPDATE_METRICS
WITH cte AS MATERIALIZED (
#{inner_query.to_sql}
)
UPDATE issue_metrics
SET
first_mentioned_in_commit_at = cte.authored_date
FROM
cte
WHERE
cte.issue_id = issue_metrics.issue_id
UPDATE_METRICS
end
mark_job_as_succeeded(start_id, end_id)
end
private
def mark_job_as_succeeded(*arguments)
Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
'FixFirstMentionedInCommitAt',
arguments
)
end
def scope(start_id, end_id)
TmpIssueMetrics.from_2020.where(issue_id: start_id..end_id)
end
def lateral_query
<<~SQL
LATERAL (
SELECT MIN(first_authored_date.authored_date) as authored_date
FROM merge_requests_closing_issues,
LATERAL (
SELECT id
FROM merge_request_diffs
WHERE merge_request_id = merge_requests_closing_issues.merge_request_id
ORDER BY id DESC
LIMIT 1
) last_diff_id,
LATERAL (
SELECT authored_date
FROM merge_request_diff_commits
WHERE
merge_request_diff_id = last_diff_id.id
ORDER BY relative_order DESC
LIMIT 1
) first_authored_date
WHERE merge_requests_closing_issues.issue_id = issue_metrics.issue_id
) first_authored_date
SQL
end
end
end
end

View File

@ -60,6 +60,8 @@ module Gitlab
end
def to_s
return if text.blank?
# Gitlab::EncodingHelper#clean remove `null` chars from the string
text = clean(format)
text = convert_ref_links(text, project) if project.present?

View File

@ -11251,6 +11251,9 @@ msgstr ""
msgid "CICD|Auto DevOps"
msgstr ""
msgid "CICD|Auto-populating allowlist entries. Please wait while the action completes."
msgstr ""
msgid "CICD|Automatic deployment to staging, manual deployment to production"
msgstr ""
@ -11371,7 +11374,7 @@ msgstr ""
msgid "CICD|The allowlist can contain a maximum of %{projectAllowlistLimit} groups and projects."
msgstr ""
msgid "CICD|The process to add entries could take a moment to complete with large logs or allowlists."
msgid "CICD|The process might take a moment to complete for large authentication logs or allowlists."
msgstr ""
msgid "CICD|There are several CI/CD limits in place."
@ -11398,7 +11401,7 @@ msgstr ""
msgid "CICD|When enabled, all projects must use their allowlist to control CI/CD job token access between projects. The option to allow access from all groups and projects is hidden. %{link_start}Learn More.%{link_end}"
msgstr ""
msgid "CICD|You're about to add all entries from the authentication log to the allowlist for %{projectName}. Duplicate entries will be ignored."
msgid "CICD|You're about to add all entries from the authentication log to the allowlist for %{projectName}. This will also update the Job Token setting to %{codeStart}This project and any groups and projects in the allowlist%{codeEnd}, if not already set. Duplicate entries will be ignored."
msgstr ""
msgid "CICD|group enabled"
@ -21671,6 +21674,18 @@ msgstr ""
msgid "DuoChat|Give feedback"
msgstr ""
msgid "DuoChat|How do I change my password in GitLab?"
msgstr ""
msgid "DuoChat|How do I clone a repository?"
msgstr ""
msgid "DuoChat|How do I create a template?"
msgstr ""
msgid "DuoChat|How do I fork a project?"
msgstr ""
msgid "DuoChat|How to use GitLab"
msgstr ""
@ -29280,24 +29295,12 @@ msgstr ""
msgid "How can I make my variables more secure?"
msgstr ""
msgid "How do I change my password in GitLab?"
msgstr ""
msgid "How do I clone a repository?"
msgstr ""
msgid "How do I configure Akismet?"
msgstr ""
msgid "How do I configure this integration?"
msgstr ""
msgid "How do I create a template?"
msgstr ""
msgid "How do I fork a project?"
msgstr ""
msgid "How do I generate it?"
msgstr ""

View File

@ -531,77 +531,6 @@ RSpec.describe GraphqlController, feature_category: :integrations do
end
end
describe 'DPoP authentication' do
context 'when :dpop_authentication FF is disabled' do
let(:user) { create(:user, last_activity_on: last_activity_on) }
let(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
it 'does not check for DPoP token' do
stub_feature_flags(dpop_authentication: false)
post :execute, params: { access_token: personal_access_token.token }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when :dpop_authentication FF is enabled' do
before do
stub_feature_flags(dpop_authentication: true)
end
context 'when DPoP is disabled for the user' do
let(:user) { create(:user, last_activity_on: last_activity_on) }
let(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
it 'does not check for DPoP token' do
post :execute, params: { access_token: personal_access_token.token }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when DPoP is enabled for the user' do
let_it_be(:user) { create(:user, last_activity_on: last_activity_on, dpop_enabled: true) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
let_it_be(:oauth_token) { create(:oauth_access_token, user: user, scopes: [:api]) }
let_it_be(:dpop_proof) { generate_dpop_proof_for(user) }
context 'when API is called with an OAuth token' do
it 'does not invoke DPoP' do
request.headers["Authorization"] = "Bearer #{oauth_token.plaintext_token}"
post :execute
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with a missing DPoP token' do
it 'returns 401' do
post :execute, params: { access_token: personal_access_token.token }
expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response["errors"][0]["message"]).to eq("DPoP validation error: DPoP header is missing")
end
end
context 'with a valid DPoP token' do
it 'returns 200' do
request.headers["dpop"] = dpop_proof.proof
post :execute, params: { access_token: personal_access_token.token }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with a malformed DPoP token' do
it 'returns 401' do
request.headers["dpop"] = "invalid"
post :execute, params: { access_token: personal_access_token.token } # -- We need the entire error message
expect(json_response["errors"][0]["message"])
.to eq("DPoP validation error: Malformed JWT, unable to decode. Not enough or too many segments")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end
end
context 'when user is not logged in' do
it 'returns 200' do
post :execute
@ -782,6 +711,88 @@ RSpec.describe GraphqlController, feature_category: :integrations do
end
end
end
describe 'DPoP authentication' do
context 'when :dpop_authentication FF is disabled' do
let(:user) { create(:user, last_activity_on: last_activity_on) }
let(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
it 'does not check for DPoP token' do
stub_feature_flags(dpop_authentication: false)
post :execute, params: { access_token: personal_access_token.token }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when :dpop_authentication FF is enabled' do
before do
stub_feature_flags(dpop_authentication: true)
end
context 'when DPoP is disabled for the user' do
let(:user) { create(:user, last_activity_on: last_activity_on) }
let(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
it 'does not check for DPoP token' do
post :execute, params: { access_token: personal_access_token.token }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when DPoP is enabled for the user' do
let_it_be(:user) { create(:user, last_activity_on: last_activity_on, dpop_enabled: true) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
let_it_be(:oauth_token) { create(:oauth_access_token, user: user, scopes: [:api]) }
let_it_be(:dpop_proof) { generate_dpop_proof_for(user) }
context 'when cookie-based authentication is used' do
it 'does not invoke DPoP' do
sign_in(user)
expect(controller).not_to receive(:extract_personal_access_token)
post :execute
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'when API is called with an OAuth token' do
it 'does not invoke DPoP' do
request.headers["Authorization"] = "Bearer #{oauth_token.plaintext_token}"
post :execute
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with a missing DPoP token' do
it 'returns 401' do
post :execute, params: { access_token: personal_access_token.token }
expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response["errors"][0]["message"]).to eq("DPoP validation error: DPoP header is missing")
end
end
context 'with a valid DPoP token' do
it 'returns 200' do
request.headers["dpop"] = dpop_proof.proof
post :execute, params: { access_token: personal_access_token.token }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'with a malformed DPoP token' do
it 'returns 401' do
request.headers["dpop"] = "invalid"
post :execute, params: { access_token: personal_access_token.token } # -- We need the entire error message
expect(json_response["errors"][0]["message"])
.to eq("DPoP validation error: Malformed JWT, unable to decode. Not enough or too many segments")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end
end
end
describe 'Admin Mode' do

View File

@ -349,7 +349,7 @@ RSpec.describe HelpController do
context 'for image formats' do
context 'when requested file exists' do
it 'renders the raw file' do
get :show, params: { path: 'user/img/markdown_logo' }, format: :png
get :show, params: { path: 'user/img/markdown_logo_v17_11' }, format: :png
aggregate_failures do
expect(response).to be_successful

View File

@ -2,6 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import PipelineDurationChart from '~/projects/pipelines/charts/components/pipeline_duration_chart.vue';
import { stubComponent } from 'helpers/stub_component';
describe('PipelineDurationChart', () => {
let wrapper;
@ -9,11 +10,12 @@ describe('PipelineDurationChart', () => {
const findLineChart = () => wrapper.findComponent(GlLineChart);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createComponent = ({ props } = {}) => {
const createComponent = ({ props, ...options } = {}) => {
wrapper = shallowMount(PipelineDurationChart, {
propsData: {
...props,
},
...options,
});
};
@ -75,4 +77,31 @@ describe('PipelineDurationChart', () => {
},
]);
});
describe('formats tooltip', () => {
const oneMinute = 60;
const oneHour = 3600;
const oneDay = oneHour * 24;
it.each`
date | value | expectedTooltip
${'2021-12-01'} | ${oneMinute} | ${'Dec 1, 2021 - 1m'}
${'2022-12-15'} | ${oneHour + oneMinute} | ${'Dec 15, 2022 - 1h 1m'}
${'2023-12-31'} | ${oneDay + oneHour + oneMinute} | ${'Dec 31, 2023 - 1d 1h 1m'}
`('$expectedTooltip', ({ date, value, expectedTooltip }) => {
createComponent({
stubs: {
GlLineChart: stubComponent(GlLineChart, {
template: `<div>
<slot name="tooltip-title" :params="{ value: '${date}' }"></slot>
-
<slot name="tooltip-value" :value="${value}"></slot>
</div>`,
}),
},
});
expect(findLineChart().text()).toMatchInterpolatedText(expectedTooltip);
});
});
});

View File

@ -2,6 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { GlStackedColumnChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import PipelineStatusChart from '~/projects/pipelines/charts/components/pipeline_status_chart.vue';
import { stubComponent } from 'helpers/stub_component';
describe('PipelineStatusChart', () => {
let wrapper;
@ -9,11 +10,12 @@ describe('PipelineStatusChart', () => {
const findStackedColumnChart = () => wrapper.findComponent(GlStackedColumnChart);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createComponent = ({ props } = {}) => {
const createComponent = ({ props, ...options } = {}) => {
wrapper = shallowMount(PipelineStatusChart, {
propsData: {
...props,
},
...options,
});
};
@ -62,4 +64,25 @@ describe('PipelineStatusChart', () => {
{ data: [30, 31], name: 'Other' },
]);
});
describe('formats tooltip', () => {
it.each`
date | expectedTooltip
${'2021-12-01'} | ${'Dec 1, 2021'}
${'2022-12-15'} | ${'Dec 15, 2022'}
${'2023-12-31'} | ${'Dec 31, 2023'}
`('$expectedTooltip', ({ date, expectedTooltip }) => {
createComponent({
stubs: {
GlStackedColumnChart: stubComponent(GlStackedColumnChart, {
template: `<div>
<slot name="tooltip-title" :params="{ value: '${date}' }"></slot>
</div>`,
}),
},
});
expect(findStackedColumnChart().text()).toMatchInterpolatedText(expectedTooltip);
});
});
});

View File

@ -1,44 +1,22 @@
import { GlAlert, GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { createMockDirective } from 'helpers/vue_mock_directive';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import AutopopulateAllowlistMutation from '~/token_access/graphql/mutations/autopopulate_allowlist.mutation.graphql';
import AutopopulateAllowlistModal from '~/token_access/components/autopopulate_allowlist_modal.vue';
import { mockAutopopulateAllowlistResponse, mockAutopopulateAllowlistError } from './mock_data';
const projectName = 'My project';
const fullPath = 'root/my-repo';
Vue.use(VueApollo);
const mockToastShow = jest.fn();
describe('AutopopulateAllowlistModal component', () => {
let wrapper;
let mockApollo;
let mockAutopopulateMutation;
const findAlert = () => wrapper.findComponent(GlAlert);
const findModal = () => wrapper.findComponent(GlModal);
const findLink = () => wrapper.findComponent(GlLink);
const createComponent = ({ props } = {}) => {
const handlers = [[AutopopulateAllowlistMutation, mockAutopopulateMutation]];
mockApollo = createMockApollo(handlers);
wrapper = shallowMountExtended(AutopopulateAllowlistModal, {
apolloProvider: mockApollo,
provide: {
fullPath,
},
mocks: {
$toast: { show: mockToastShow },
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
propsData: {
authLogExceedsLimit: false,
projectAllowlistLimit: 4,
@ -52,10 +30,6 @@ describe('AutopopulateAllowlistModal component', () => {
});
};
beforeEach(() => {
mockAutopopulateMutation = jest.fn();
});
describe('template', () => {
beforeEach(() => {
createComponent();
@ -90,7 +64,7 @@ describe('AutopopulateAllowlistModal component', () => {
it('renders help link', () => {
expect(findLink().text()).toBe('What is the compaction algorithm?');
expect(findLink().attributes('href')).toBe(
'/help/ci/jobs/ci_job_token#auto-populate-a-projects-allowlist',
'/help/ci/jobs/ci_job_token#allowlist-compaction',
);
});
});
@ -112,66 +86,15 @@ describe('AutopopulateAllowlistModal component', () => {
);
});
describe('when mutation is running', () => {
beforeEach(() => {
mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistResponse);
describe('when clicking on the primary button', () => {
it('emits the remove-entries event', () => {
createComponent();
});
it('shows loading state for confirm button and disables cancel button', async () => {
expect(findModal().props('actionPrimary').attributes).toMatchObject({ loading: false });
expect(findModal().props('actionSecondary').attributes).toMatchObject({ disabled: false });
expect(wrapper.emitted('autopopulate-allowlist')).toBeUndefined();
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await nextTick();
expect(findModal().props('actionPrimary').attributes).toMatchObject({ loading: true });
expect(findModal().props('actionSecondary').attributes).toMatchObject({ disabled: true });
});
});
describe('when mutation is successful', () => {
beforeEach(async () => {
mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistResponse);
createComponent();
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
});
it('calls the mutation', () => {
expect(mockAutopopulateMutation).toHaveBeenCalledWith({ projectPath: fullPath });
});
it('shows toast message', () => {
expect(mockToastShow).toHaveBeenCalledWith(
'Authentication log entries were successfully added to the allowlist.',
);
});
it('emits events for refetching data and hiding modal', () => {
expect(wrapper.emitted('refetch-allowlist')).toHaveLength(1);
expect(wrapper.emitted('hide')).toHaveLength(1);
});
});
describe('when mutation fails', () => {
beforeEach(async () => {
createComponent();
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistError);
});
it('renders alert', () => {
expect(findAlert().exists()).toBe(true);
});
it('does not render toast message or emit events', () => {
expect(mockToastShow).not.toHaveBeenCalledWith();
expect(wrapper.emitted('refetch-allowlist')).toBeUndefined();
expect(wrapper.emitted('hide')).toBeUndefined();
expect(wrapper.emitted('autopopulate-allowlist')).toHaveLength(1);
});
});
});

View File

@ -18,6 +18,7 @@ import {
import AutopopulateAllowlistModal from '~/token_access/components/autopopulate_allowlist_modal.vue';
import NamespaceForm from '~/token_access/components/namespace_form.vue';
import RemoveAutopopulatedEntriesModal from '~/token_access/components/remove_autopopulated_entries_modal.vue';
import autopopulateAllowlistMutation from '~/token_access/graphql/mutations/autopopulate_allowlist.mutation.graphql';
import inboundRemoveGroupCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql';
import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql';
import inboundUpdateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql';
@ -38,6 +39,7 @@ import {
inboundRemoveNamespaceSuccess,
inboundUpdateScopeSuccessResponse,
mockAuthLogsCountResponse,
mockAutopopulateAllowlistResponse,
mockRemoveAutopopulatedEntriesResponse,
} from './mock_data';
@ -53,6 +55,13 @@ describe('TokenAccess component', () => {
let wrapper;
const authLogCountResponseHandler = jest.fn().mockResolvedValue(mockAuthLogsCountResponse(4));
const authLogZeroCountResponseHandler = jest.fn().mockResolvedValue(mockAuthLogsCountResponse(0));
const autopopulateAllowlistResponseHandler = jest
.fn()
.mockResolvedValue(mockAutopopulateAllowlistResponse());
const autopopulateAllowlistResponseErrorHandler = jest
.fn()
.mockResolvedValue(mockAutopopulateAllowlistResponse({ errorMessage: message }));
const inboundJobTokenScopeEnabledResponseHandler = jest
.fn()
.mockResolvedValue(inboundJobTokenScopeEnabledResponse);
@ -61,7 +70,10 @@ describe('TokenAccess component', () => {
.mockResolvedValue(inboundJobTokenScopeDisabledResponse);
const inboundGroupsAndProjectsWithScopeResponseHandler = jest
.fn()
.mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse);
.mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse(true));
const inboundGroupsAndProjectsWithoutAutopopulatedEntriesResponseHandler = jest
.fn()
.mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse(false));
const inboundRemoveGroupSuccessHandler = jest
.fn()
.mockResolvedValue(inboundRemoveNamespaceSuccess);
@ -474,7 +486,9 @@ describe('TokenAccess component', () => {
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[autopopulateAllowlistMutation, autopopulateAllowlistResponseHandler],
[removeAutopopulatedEntriesMutation, removeAutopopulatedEntriesMutationHandler],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
@ -515,15 +529,112 @@ describe('TokenAccess component', () => {
expect(findFormSelector().props('selected')).toBe(null);
});
it('refetches allowlist when autopopulate mutation is successful', async () => {
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(1);
it('shows loading state while autopopulating entries', async () => {
expect(findCountLoadingIcon().exists()).toBe(false);
expect(findTokenAccessTable().props('loading')).toBe(false);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('refetch-allowlist');
findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
await nextTick();
expect(findCountLoadingIcon().exists()).toBe(true);
expect(findTokenAccessTable().props('loading')).toBe(true);
expect(findTokenAccessTable().props('loadingMessage')).toBe(
'Auto-populating allowlist entries. Please wait while the action completes.',
);
});
it('resets loading state after autopopulating entries', async () => {
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
await nextTick();
expect(findTokenAccessTable().props('loadingMessage')).toBe(
'Auto-populating allowlist entries. Please wait while the action completes.',
);
await waitForPromises();
expect(findCountLoadingIcon().exists()).toBe(false);
expect(findTokenAccessTable().props('loading')).toBe(false);
expect(findTokenAccessTable().props('loadingMessage')).toBe('');
});
it('calls the autopopulate allowlist mutation and refetches allowlist and job token setting', async () => {
expect(autopopulateAllowlistResponseHandler).toHaveBeenCalledTimes(0);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(1);
expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledTimes(1);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
await waitForPromises();
await nextTick();
expect(autopopulateAllowlistResponseHandler).toHaveBeenCalledTimes(1);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(2);
expect(findFormSelector().props('selected')).toBe(null);
expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledTimes(2);
});
it('shows error alert when mutation returns an error', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[autopopulateAllowlistMutation, autopopulateAllowlistResponseErrorHandler],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
);
await waitForPromises();
expect(findAutopopulationAlert().exists()).toBe(false);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
await waitForPromises();
await nextTick();
expect(findAutopopulationAlert().text()).toBe('An error occurred');
});
it('shows error alert when mutation fails', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[autopopulateAllowlistMutation, failureHandler],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
);
await waitForPromises();
expect(findAutopopulationAlert().exists()).toBe(false);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
await waitForPromises();
await nextTick();
expect(findAutopopulationAlert().text()).toBe(
'An error occurred while adding the authentication log entries. Please try again.',
);
});
});
@ -561,6 +672,21 @@ describe('TokenAccess component', () => {
);
});
it('resets loading state after removing autopopulated entries', async () => {
triggerRemoveEntries();
await nextTick();
expect(findTokenAccessTable().props('loadingMessage')).toBe(
'Removing auto-added allowlist entries. Please wait while the action completes.',
);
await waitForPromises();
expect(findCountLoadingIcon().exists()).toBe(false);
expect(findTokenAccessTable().props('loading')).toBe(false);
expect(findTokenAccessTable().props('loadingMessage')).toBe('');
});
it('calls the remove autopopulated entries mutation and refetches allowlist', async () => {
expect(removeAutopopulatedEntriesMutationHandler).toHaveBeenCalledTimes(0);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(1);
@ -592,6 +718,7 @@ describe('TokenAccess component', () => {
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[removeAutopopulatedEntriesMutation, removeAutopopulatedEntriesMutationErrorHandler],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
@ -599,6 +726,8 @@ describe('TokenAccess component', () => {
},
);
await waitForPromises();
expect(findAutopopulationAlert().exists()).toBe(false);
triggerRemoveEntries();
@ -617,6 +746,7 @@ describe('TokenAccess component', () => {
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[removeAutopopulatedEntriesMutation, failureHandler],
[getAuthLogCountQuery, authLogCountResponseHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
@ -624,6 +754,8 @@ describe('TokenAccess component', () => {
},
);
await waitForPromises();
expect(findAutopopulationAlert().exists()).toBe(false);
triggerRemoveEntries();
@ -652,6 +784,39 @@ describe('TokenAccess component', () => {
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(true);
});
});
describe('allowlist actions', () => {
beforeEach(async () => {
await createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
[
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithoutAutopopulatedEntriesResponseHandler,
],
[getAuthLogCountQuery, authLogZeroCountResponseHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
},
);
await nextTick();
});
it('hides add auth log entries option if auth log count is zero', () => {
expect(findFormSelector().props('items')).toMatchObject([
{
text: 'Group or project',
value: 'JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT',
},
]);
});
it('hides remove auth log entries option if there are no autopopulated entries', () => {
expect(findAllowlistOptions().exists()).toBe(false);
});
});
});
describe.each`

View File

@ -164,7 +164,7 @@ export const inboundJobTokenScopeDisabledResponse = {
},
};
export const inboundGroupsAndProjectsWithScopeResponse = {
export const inboundGroupsAndProjectsWithScopeResponse = (hasAutopopulatedEntries = true) => ({
data: {
project: {
__typename: 'Project',
@ -197,12 +197,14 @@ export const inboundGroupsAndProjectsWithScopeResponse = {
},
],
},
groupAllowlistAutopopulatedIds: ['gid://gitlab/Group/45'],
inboundAllowlistAutopopulatedIds: ['gid://gitlab/Project/23'],
groupAllowlistAutopopulatedIds: hasAutopopulatedEntries ? ['gid://gitlab/Group/45'] : [],
inboundAllowlistAutopopulatedIds: hasAutopopulatedEntries
? ['gid://gitlab/Project/23']
: [],
},
},
},
};
});
export const getSaveNamespaceHandler = (error) =>
jest.fn().mockResolvedValue({
@ -301,28 +303,15 @@ export const mockAuthLogsResponse = (hasNextPage = false) => ({
},
});
export const mockAutopopulateAllowlistResponse = {
export const mockAutopopulateAllowlistResponse = ({ errorMessage } = {}) => ({
data: {
ciJobTokenScopeAutopopulateAllowlist: {
status: 'complete',
errors: [],
errors: errorMessage ? [{ message: errorMessage }] : [],
__typename: 'CiJobTokenScopeAutopopulateAllowlistPayload',
},
},
};
export const mockAutopopulateAllowlistError = {
data: {
ciJobTokenScopeAutopopulateAllowlist: {
errors: [
{
message: 'An error occurred',
},
],
__typename: 'CiJobTokenScopeAutopopulateAllowlistPayload',
},
},
};
});
export const mockRemoveAutopopulatedEntriesResponse = ({ errorMessage } = {}) => ({
data: {

View File

@ -78,6 +78,39 @@ RSpec.describe Ci::RunnersHelper, feature_category: :fleet_visibility do
end
end
describe '#admin_runners_fleet_dashboard_data', :enable_admin_mode do
let_it_be(:user) { admin_user }
subject(:data) { helper.admin_runners_fleet_dashboard_data }
it 'returns correct data' do
expect(data).to include(
admin_runners_path: '/admin/runners',
new_runner_path: '/admin/runners/new',
clickhouse_ci_analytics_available: 'false',
can_admin_runners: 'true'
)
end
context 'when ClickHouse is configured' do
before do
allow(Gitlab::ClickHouse).to receive(:configured?).and_return(true)
end
it 'returns the correct data' do
expect(data).to include(clickhouse_ci_analytics_available: 'true')
end
end
context 'when current user is not an admin' do
let_it_be(:user) { non_admin_user }
it 'returns the correct data' do
expect(data).to include(can_admin_runners: 'false')
end
end
end
describe '#group_shared_runners_settings_data' do
let_it_be(:parent) { create(:group) }
let_it_be(:group) { create(:group, parent: parent, shared_runners_enabled: false) }

View File

@ -124,13 +124,6 @@ RSpec.describe Gitlab::GithubImport::MarkdownText, feature_category: :importers
expect(text.to_s).to eq('Hello')
end
it 'returns empty text when it receives nil' do
author = double(:author, login: nil)
text = described_class.new(nil, author, true)
expect(text.to_s).to eq('')
end
it 'returns the text with an extra header when the author was not found' do
author = double(:author, login: 'Alice')
text = described_class.new('Hello', author)
@ -150,11 +143,28 @@ RSpec.describe Gitlab::GithubImport::MarkdownText, feature_category: :importers
let(:text) { "I said to @sam_allen\0 the code" }
let(:instance) { described_class.new(text, project:) }
subject(:format) { instance.to_s }
it 'calls wrap_mentions_in_backticks and convert_ref_links method as a cleaning step' do
expect(instance).to receive(:wrap_mentions_in_backticks)
expect(instance).to receive(:convert_ref_links)
instance.to_s
format
end
context "when the text is blank?" do
let(:text) { nil }
it "skips formatting" do
expect(instance).not_to receive(:wrap_mentions_in_backticks)
expect(instance).not_to receive(:convert_ref_links)
format
end
it "returns nil as response" do
expect(format).to be_nil
end
end
end
end

View File

@ -115,8 +115,8 @@ RSpec.describe HelpController, "routing" do
path = '/help/user/markdown.md'
expect(get(path)).to route_to('help#show', path: 'user/markdown', format: 'md')
path = '/help/user/markdown/markdown_logo.png'
expect(get(path)).to route_to('help#show', path: 'user/markdown/markdown_logo', format: 'png')
path = '/help/user/markdown/markdown_logo_v17_11.png'
expect(get(path)).to route_to('help#show', path: 'user/markdown/markdown_logo_v17_11', format: 'png')
end
end

View File

@ -9,12 +9,7 @@ internal/config/config.go:247:18: G204: Subprocess launched with variable (gosec
internal/config/config.go:339:8: G101: Potential hardcoded credentials (gosec)
internal/dependencyproxy/dependencyproxy.go:121: Function 'Inject' is too long (61 > 60) (funlen)
internal/dependencyproxy/dependencyproxy_test.go:514: internal/dependencyproxy/dependencyproxy_test.go:514: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "note that the timeout duration here is s..." (godox)
internal/git/archive.go:39:2: var-naming: struct field CommitId should be CommitID (revive)
internal/git/archive.go:49:2: exported: exported var SendArchive should have comment or be unexported (revive)
internal/git/archive.go:66: Function 'Inject' has too many statements (49 > 40) (funlen)
internal/git/archive.go:90:29: Error return value of `cachedArchive.Close` is not checked (errcheck)
internal/git/archive.go:116:23: Error return value of `tempFile.Close` is not checked (errcheck)
internal/git/archive.go:117:18: Error return value of `os.Remove` is not checked (errcheck)
internal/git/archive.go:67: Function 'Inject' has too many statements (55 > 40) (funlen)
internal/git/blob.go:21:5: exported: exported var SendBlob should have comment or be unexported (revive)
internal/git/diff.go:1: 1-47 lines are duplicate of `internal/git/format-patch.go:1-48` (dupl)
internal/git/diff.go:22:5: exported: exported var SendDiff should have comment or be unexported (revive)

View File

@ -36,7 +36,7 @@ type archive struct {
type archiveParams struct {
ArchivePath string
ArchivePrefix string
CommitId string
CommitID string
GitalyServer api.GitalyServer
GitalyRepository gitalypb.Repository
DisableCache bool
@ -46,6 +46,7 @@ type archiveParams struct {
}
var (
// SendArchive sends a Git archive to the client, retrieving from the local disk cache if available.
SendArchive = newArchive("git-archive:")
gitArchiveCache = promauto.NewCounterVec(
prometheus.CounterOpts{
@ -87,7 +88,12 @@ func (a *archive) Inject(w http.ResponseWriter, r *http.Request, sendData string
cachedArchive, err := os.Open(params.ArchivePath)
if err == nil {
defer cachedArchive.Close()
defer func() {
err = cachedArchive.Close()
if err != nil {
log.WithError(err).Error("SendArchive: failed to close cached archive")
}
}()
gitArchiveCache.WithLabelValues("hit").Inc()
setArchiveHeaders(w, format, archiveFilename)
// Even if somebody deleted the cachedArchive from disk since we opened
@ -113,8 +119,15 @@ func (a *archive) Inject(w http.ResponseWriter, r *http.Request, sendData string
fail.Request(w, r, fmt.Errorf("SendArchive: create tempfile: %v", err))
return
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
defer func() {
// Ignore error, this may have already been closed with finalizeCachedArchive
_ = tempFile.Close()
err = os.Remove(tempFile.Name())
if err != nil {
log.WithError(err).Error("SendArchive: failed to remove tempfile")
}
}()
}
var archiveReader io.Reader
@ -175,7 +188,7 @@ func handleArchiveWithGitaly(r *http.Request, params *archiveParams, format gita
} else {
request = &gitalypb.GetArchiveRequest{
Repository: &params.GitalyRepository,
CommitId: params.CommitId,
CommitId: params.CommitID,
Prefix: params.ArchivePrefix,
Format: format,
}