mirror of
https://github.com/gitlabhq/gitlabhq.git
synced 2025-07-21 23:37:47 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
@ -3185,7 +3185,6 @@ Gitlab/BoundedContexts:
|
||||
- 'ee/app/services/epic_issues/list_service.rb'
|
||||
- 'ee/app/services/epic_issues/update_service.rb'
|
||||
- 'ee/app/services/epics/base_service.rb'
|
||||
- 'ee/app/services/epics/close_service.rb'
|
||||
- 'ee/app/services/epics/descendant_count_service.rb'
|
||||
- 'ee/app/services/epics/epic_links/destroy_service.rb'
|
||||
- 'ee/app/services/epics/epic_links/list_service.rb'
|
||||
|
@ -279,7 +279,6 @@ Layout/EmptyLineAfterMagicComment:
|
||||
- 'ee/spec/services/ee/design_management/save_designs_service_spec.rb'
|
||||
- 'ee/spec/services/ee/notes/quick_actions_service_spec.rb'
|
||||
- 'ee/spec/services/ee/users/update_service_spec.rb'
|
||||
- 'ee/spec/services/epics/close_service_spec.rb'
|
||||
- 'ee/spec/services/epics/issue_promote_service_spec.rb'
|
||||
- 'ee/spec/services/issue_feature_flags/list_service_spec.rb'
|
||||
- 'ee/spec/services/milestones/update_service_spec.rb'
|
||||
|
@ -543,7 +543,6 @@ RSpec/BeforeAllRoleAssignment:
|
||||
- 'ee/spec/services/ee/two_factor/destroy_service_spec.rb'
|
||||
- 'ee/spec/services/ee/work_items/import_csv_service_spec.rb'
|
||||
- 'ee/spec/services/epic_issues/update_service_spec.rb'
|
||||
- 'ee/spec/services/epics/close_service_spec.rb'
|
||||
- 'ee/spec/services/epics/issue_promote_service_spec.rb'
|
||||
- 'ee/spec/services/epics/related_epic_links/list_service_spec.rb'
|
||||
- 'ee/spec/services/epics/transfer_service_spec.rb'
|
||||
|
@ -11,7 +11,6 @@ RSpec/ExpectInLet:
|
||||
- 'ee/spec/services/ee/merge_requests/reopen_service_spec.rb'
|
||||
- 'ee/spec/services/ee/notes/create_service_spec.rb'
|
||||
- 'ee/spec/services/ee/users/migrate_records_to_ghost_user_service_spec.rb'
|
||||
- 'ee/spec/services/epics/close_service_spec.rb'
|
||||
- 'ee/spec/services/groups/destroy_service_spec.rb'
|
||||
- 'ee/spec/services/projects/destroy_service_spec.rb'
|
||||
- 'ee/spec/services/projects/group_links/destroy_service_spec.rb'
|
||||
|
@ -869,7 +869,6 @@ RSpec/NamedSubject:
|
||||
- 'ee/spec/services/epic_issues/destroy_service_spec.rb'
|
||||
- 'ee/spec/services/epic_issues/list_service_spec.rb'
|
||||
- 'ee/spec/services/epic_issues/update_service_spec.rb'
|
||||
- 'ee/spec/services/epics/close_service_spec.rb'
|
||||
- 'ee/spec/services/epics/epic_links/list_service_spec.rb'
|
||||
- 'ee/spec/services/epics/issue_promote_service_spec.rb'
|
||||
- 'ee/spec/services/epics/related_epic_links/destroy_service_spec.rb'
|
||||
|
@ -1 +1 @@
|
||||
ab88e3894dc8f759c768d0324b2614c7274dd56d
|
||||
2b6810dc5688ebdbf4024b4885fdc59686a3f1fa
|
||||
|
@ -1,9 +1,10 @@
|
||||
<script>
|
||||
import { GlButton, GlFormCheckbox, GlLink, GlAlert } from '@gitlab/ui';
|
||||
import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
|
||||
import lintCiMutation from '~/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
|
||||
import ciLintMutation from '~/ci/pipeline_editor/graphql/mutations/ci_lint.mutation.graphql';
|
||||
import SourceEditor from '~/vue_shared/components/source_editor.vue';
|
||||
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
|
||||
import { CI_CONFIG_STATUS_VALID } from '~/ci/pipeline_editor/constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -16,10 +17,6 @@ export default {
|
||||
HelpIcon,
|
||||
},
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
lintHelpPagePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
@ -28,6 +25,10 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectFullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -52,19 +53,25 @@ export default {
|
||||
async lint() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
lintCI: { valid, errors, warnings, jobs },
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: ciLintMutation,
|
||||
variables: {
|
||||
projectPath: this.projectFullPath,
|
||||
content: this.content,
|
||||
dryRun: this.dryRun,
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: lintCiMutation,
|
||||
variables: { endpoint: this.endpoint, content: this.content, dry: this.dryRun },
|
||||
});
|
||||
|
||||
const ciConfigData = data?.ciLint?.config || {};
|
||||
const { errors, stages, warnings, status } = ciConfigData;
|
||||
|
||||
this.showingResults = true;
|
||||
this.isValid = valid;
|
||||
this.isValid = status === CI_CONFIG_STATUS_VALID;
|
||||
this.errors = errors;
|
||||
this.warnings = warnings;
|
||||
const jobs = stages.flatMap((stage) =>
|
||||
(stage.groups || []).flatMap((group) => group.jobs || []),
|
||||
);
|
||||
this.jobs = jobs;
|
||||
} catch (error) {
|
||||
this.apiError = error;
|
||||
|
@ -13,7 +13,7 @@ const apolloProvider = new VueApollo({
|
||||
|
||||
export default (containerId = '#js-ci-lint') => {
|
||||
const containerEl = document.querySelector(containerId);
|
||||
const { endpoint, lintHelpPagePath, pipelineSimulationHelpPagePath } = containerEl.dataset;
|
||||
const { lintHelpPagePath, pipelineSimulationHelpPagePath, projectFullPath } = containerEl.dataset;
|
||||
|
||||
return new Vue({
|
||||
el: containerEl,
|
||||
@ -21,9 +21,9 @@ export default (containerId = '#js-ci-lint') => {
|
||||
render(createElement) {
|
||||
return createElement(CiLint, {
|
||||
props: {
|
||||
endpoint,
|
||||
lintHelpPagePath,
|
||||
pipelineSimulationHelpPagePath,
|
||||
projectFullPath,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
@ -14,12 +14,12 @@ import { s__, __ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
|
||||
import { pipelineEditorTrackingOptions } from '../../constants';
|
||||
import { pipelineEditorTrackingOptions, CI_CONFIG_STATUS_VALID } from '../../constants';
|
||||
import ValidatePipelinePopover from '../popovers/validate_pipeline_popover.vue';
|
||||
import CiLintResults from '../lint/ci_lint_results.vue';
|
||||
import getBlobContent from '../../graphql/queries/blob_content.query.graphql';
|
||||
import getCurrentBranch from '../../graphql/queries/client/current_branch.query.graphql';
|
||||
import lintCiMutation from '../../graphql/mutations/client/lint_ci.mutation.graphql';
|
||||
import ciLintMutation from '../../graphql/mutations/ci_lint.mutation.graphql';
|
||||
|
||||
export const i18n = {
|
||||
alertDesc: s__(
|
||||
@ -159,25 +159,29 @@ export default {
|
||||
this.state = VALIDATE_TAB_LOADING;
|
||||
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
lintCI: { errors, jobs, valid, warnings },
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: lintCiMutation,
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: ciLintMutation,
|
||||
variables: {
|
||||
dry: true,
|
||||
projectPath: this.projectFullPath,
|
||||
content: this.yaml,
|
||||
endpoint: this.ciLintPath,
|
||||
ref: this.currentBranch,
|
||||
dryRun: true,
|
||||
},
|
||||
});
|
||||
|
||||
const ciConfigData = data?.ciLint?.config || {};
|
||||
|
||||
// only save the result if the user did not cancel the simulation
|
||||
if (this.state === VALIDATE_TAB_LOADING) {
|
||||
const { errors, stages, warnings, status } = ciConfigData;
|
||||
|
||||
this.errors = errors;
|
||||
const jobs = stages.flatMap((stage) =>
|
||||
(stage.groups || []).flatMap((group) => group.jobs || []),
|
||||
);
|
||||
this.jobs = jobs;
|
||||
this.warnings = warnings;
|
||||
this.isValid = valid;
|
||||
this.isValid = status === CI_CONFIG_STATUS_VALID;
|
||||
this.state = VALIDATE_TAB_RESULTS;
|
||||
this.hasCiContentChanged = false;
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
mutation lintCI($endpoint: String, $content: String, $dry: Boolean) {
|
||||
lintCI(endpoint: $endpoint, content: $content, dry_run: $dry) @client {
|
||||
valid
|
||||
errors
|
||||
warnings
|
||||
jobs {
|
||||
afterScript
|
||||
allowFailure
|
||||
beforeScript
|
||||
environment
|
||||
except
|
||||
name
|
||||
only {
|
||||
refs
|
||||
}
|
||||
stage
|
||||
tags
|
||||
when
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import getAppStatus from './queries/client/app_status.query.graphql';
|
||||
import getCurrentBranch from './queries/client/current_branch.query.graphql';
|
||||
import getLastCommitBranch from './queries/client/last_commit_branch.query.graphql';
|
||||
@ -6,38 +5,6 @@ import getPipelineEtag from './queries/client/pipeline_etag.query.graphql';
|
||||
|
||||
export const resolvers = {
|
||||
Mutation: {
|
||||
lintCI: (_, { endpoint, content, dry_run }) => {
|
||||
return axios.post(endpoint, { content, dry_run }).then(({ data }) => {
|
||||
const { errors, warnings, valid, jobs } = data;
|
||||
|
||||
return {
|
||||
valid,
|
||||
errors,
|
||||
warnings,
|
||||
jobs: jobs.map((job) => {
|
||||
const only = job.only
|
||||
? { refs: job.only.refs, __typename: 'CiLintJobOnlyPolicy' }
|
||||
: null;
|
||||
|
||||
return {
|
||||
name: job.name,
|
||||
stage: job.stage,
|
||||
beforeScript: job.before_script,
|
||||
script: job.script,
|
||||
afterScript: job.after_script,
|
||||
tags: job.tag_list,
|
||||
environment: job.environment,
|
||||
when: job.when,
|
||||
allowFailure: job.allow_failure,
|
||||
only,
|
||||
except: job.except,
|
||||
__typename: 'CiLintJob',
|
||||
};
|
||||
}),
|
||||
__typename: 'CiLintContent',
|
||||
};
|
||||
});
|
||||
},
|
||||
updateAppStatus: (_, { appStatus }, { cache }) => {
|
||||
cache.writeQuery({
|
||||
query: getAppStatus,
|
||||
|
@ -2,6 +2,6 @@ import { formatGraphQLGroups } from '~/vue_shared/components/groups_list/formatt
|
||||
|
||||
export const formatGroups = (groups) =>
|
||||
formatGraphQLGroups(groups, (group) => ({
|
||||
editPath: `${group.relativeWebUrl}/-/edit`,
|
||||
editPath: `${group.webUrl}/-/edit`,
|
||||
avatarLabel: group.name,
|
||||
}));
|
||||
|
@ -136,7 +136,7 @@ export default {
|
||||
return `@${this.user?.username}`;
|
||||
},
|
||||
cssClasses() {
|
||||
const classList = ['user-popover', 'gl-max-w-48', 'gl-overflow-hidden'];
|
||||
const classList = ['user-popover', 'gl-w-34', 'gl-overflow-hidden'];
|
||||
|
||||
if (this.userCannotMerge) {
|
||||
classList.push('user-popover-cannot-merge');
|
||||
|
@ -189,21 +189,6 @@ $avatar-sizes: (
|
||||
}
|
||||
|
||||
.user-popover {
|
||||
// GlAvatarLabeled doesn't expose any prop to override internal classes
|
||||
|
||||
// Max width of popover container is set by gl-max-w-48
|
||||
// so we need to ensure that name/username/status container doesn't overflow
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-avatar-labeled-labels {
|
||||
max-width: px-to-rem(290px);
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line gitlab/no-gl-class
|
||||
.gl-avatar-labeled-label,
|
||||
.gl-avatar-labeled-sublabel {
|
||||
@apply gl-truncate;
|
||||
}
|
||||
|
||||
&.user-popover-cannot-merge {
|
||||
.popover-header {
|
||||
background-color: var(--gl-feedback-warning-background-color);
|
||||
|
@ -9,8 +9,6 @@ class ProjectStatistics < ApplicationRecord
|
||||
attribute :wiki_size, default: 0
|
||||
attribute :snippets_size, default: 0
|
||||
|
||||
ignore_column :vulnerability_count, remove_with: '17.7', remove_after: '2024-11-15'
|
||||
|
||||
counter_attribute :build_artifacts_size
|
||||
counter_attribute :packages_size
|
||||
|
||||
|
@ -3,4 +3,4 @@
|
||||
|
||||
%h4.pt-3.pb-3= _("Validate your GitLab CI configuration")
|
||||
|
||||
#js-ci-lint{ data: { endpoint: project_ci_lint_path(@project), pipeline_simulation_help_page_path: help_page_path('ci/yaml/lint.md', anchor: 'simulate-a-pipeline') , lint_help_page_path: help_page_path('ci/yaml/lint.md', anchor: 'check-cicd-syntax') } }
|
||||
#js-ci-lint{ data: { pipeline_simulation_help_page_path: help_page_path('ci/yaml/lint.md', anchor: 'simulate-a-pipeline') , lint_help_page_path: help_page_path('ci/yaml/lint.md', anchor: 'check-cicd-syntax'), project_full_path: @project.full_path } }
|
||||
|
@ -1,8 +1,9 @@
|
||||
---
|
||||
migration_job_name: DeleteOrphanedRoutes
|
||||
description: Deletes the orphaned routes that were not deleted by the loose foreign key
|
||||
description: Deletes the orphaned routes that were not deleted by the loose foreign
|
||||
key
|
||||
feature_category: groups_and_projects
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186659
|
||||
milestone: '17.11'
|
||||
queued_migration_version: 20250401113424
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
finalized_by: '20250717232515'
|
||||
|
@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeHkDeleteOrphanedRoutes < Gitlab::Database::Migration[2.3]
|
||||
milestone '18.3'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'DeleteOrphanedRoutes',
|
||||
table_name: :routes,
|
||||
column_name: :id,
|
||||
job_arguments: [],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
|
||||
def down; end
|
||||
end
|
1
db/schema_migrations/20250717232515
Normal file
1
db/schema_migrations/20250717232515
Normal file
@ -0,0 +1 @@
|
||||
fe8ddd9c103327d9e10e8c55deb3d581470ab8f5ff6b86958bfb3a613f21520f
|
@ -2121,6 +2121,7 @@ Input type: `AdminSidekiqQueuesDeleteJobsInput`
|
||||
| <a id="mutationadminsidekiqqueuesdeletejobsclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||
| <a id="mutationadminsidekiqqueuesdeletejobsfeaturecategory"></a>`featureCategory` | [`String`](#string) | Delete jobs matching feature_category in the context metadata. |
|
||||
| <a id="mutationadminsidekiqqueuesdeletejobsjobid"></a>`jobId` | [`String`](#string) | Delete jobs matching job_id in the context metadata. |
|
||||
| <a id="mutationadminsidekiqqueuesdeletejobskubernetesagentid"></a>`kubernetesAgentId` | [`String`](#string) | Delete jobs matching kubernetes_agent_id in the context metadata. |
|
||||
| <a id="mutationadminsidekiqqueuesdeletejobsmergeactionstatus"></a>`mergeActionStatus` | [`String`](#string) | Delete jobs matching merge_action_status in the context metadata. |
|
||||
| <a id="mutationadminsidekiqqueuesdeletejobsorganizationid"></a>`organizationId` | [`String`](#string) | Delete jobs matching organization_id in the context metadata. |
|
||||
| <a id="mutationadminsidekiqqueuesdeletejobspipelineid"></a>`pipelineId` | [`String`](#string) | Delete jobs matching pipeline_id in the context metadata. |
|
||||
|
@ -372,6 +372,69 @@ You can show scanner findings in the diff. For details, see:
|
||||
- [Code Quality findings](../../../ci/testing/code_quality.md#merge-request-changes-view)
|
||||
- [Static Analysis findings](../../application_security/sast/_index.md#merge-request-changes-view)
|
||||
|
||||
## Download merge request changes
|
||||
|
||||
You can download the changes included in a merge request for use outside of GitLab.
|
||||
|
||||
### As a diff
|
||||
|
||||
To download the changes as a diff:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Code > Merge requests** and find your merge request.
|
||||
1. Select the merge request.
|
||||
1. In the upper-right corner, select **Code > Plain diff**.
|
||||
|
||||
If you know the URL of the merge request, you can also download the diff from
|
||||
the command line by appending `.diff` to the URL. This example downloads the diff
|
||||
for merge request `000000`:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.diff
|
||||
```
|
||||
|
||||
To download and apply the diff in a one-line CLI command:
|
||||
|
||||
```shell
|
||||
curl "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.diff" | git apply
|
||||
```
|
||||
|
||||
### As a patch file
|
||||
|
||||
To download the changes as a patch file:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Code > Merge requests** and find your merge request.
|
||||
1. Select the merge request.
|
||||
1. In the upper-right corner, select **Code > Patches**.
|
||||
|
||||
If you know the URL of the merge request, you can also download the patch from
|
||||
the command line by appending `.patch` to the URL. This example downloads the patch
|
||||
file for merge request `000000`:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.patch
|
||||
```
|
||||
|
||||
To download and apply the patch using [`git am`](https://git-scm.com/docs/git-am):
|
||||
|
||||
```shell
|
||||
# Download and preview the patch
|
||||
curl "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.patch" > changes.patch
|
||||
git apply --check changes.patch
|
||||
|
||||
# Apply the patch
|
||||
git am changes.patch
|
||||
```
|
||||
|
||||
You can also download and apply the patch in a single command:
|
||||
|
||||
```shell
|
||||
curl "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.patch" | git am
|
||||
```
|
||||
|
||||
The `git am` uses the `-p1` option by default. For more information, see [`git-apply`](https://git-scm.com/docs/git-apply).
|
||||
|
||||
## Add a comment to a merge request file
|
||||
|
||||
{{< history >}}
|
||||
@ -400,3 +463,10 @@ This comment can also be a thread.
|
||||
1. Select the location where you want to comment.
|
||||
|
||||
GitLab shows an icon and a comment field on the image.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Compare branches](../repository/branches/_index.md#compare-branches)
|
||||
- [Download branch comparisons](../repository/branches/_index.md#download-branch-comparisons)
|
||||
- [Merge request reviews](reviews/_index.md)
|
||||
- [Merge request versions](versions.md)
|
||||
|
@ -295,51 +295,9 @@ another user with permission to merge the merge request can override this check:
|
||||
|
||||
## Download merge request changes
|
||||
|
||||
### As a diff
|
||||
|
||||
To download the changes included in a merge request as a diff:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Code > Merge requests**.
|
||||
1. Select your merge request.
|
||||
1. In the upper-right corner, select **Code > Plain diff**.
|
||||
|
||||
If you know the URL of the merge request, you can also download the diff from
|
||||
the command line by appending `.diff` to the URL. This example downloads the diff
|
||||
for merge request `000000`:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.diff
|
||||
```
|
||||
|
||||
To download and apply the diff in a one-line CLI command:
|
||||
|
||||
```shell
|
||||
curl "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.diff" | git apply
|
||||
```
|
||||
|
||||
### As a patch file
|
||||
|
||||
To download the changes included in a merge request as a patch file:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Code > Merge requests**.
|
||||
1. Select your merge request.
|
||||
1. In the upper-right corner, select **Code > Patches**.
|
||||
|
||||
If you know the URL of the merge request, you can also download the patch from
|
||||
the command line by appending `.patch` to the URL. This example downloads the patch
|
||||
file for merge request `000000`:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.patch
|
||||
```
|
||||
|
||||
To download and apply the patch in a one-line CLI command using [`git am`](https://git-scm.com/docs/git-am):
|
||||
|
||||
```shell
|
||||
curl "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.patch" | git am
|
||||
```
|
||||
You can download the changes from a merge request as a diff or patch file.
|
||||
For more information and examples, see
|
||||
[Download merge request changes](../changes.md#download-merge-request-changes).
|
||||
|
||||
## Associated features
|
||||
|
||||
@ -347,6 +305,8 @@ Merge requests are related to these features:
|
||||
|
||||
- [Cherry-pick changes](../cherry_pick_changes.md):
|
||||
In the GitLab UI, select **Cherry-pick** in a merged merge request or a commit to cherry-pick it.
|
||||
- [Compare changes](../changes.md):
|
||||
View and download the diff of changes included in a merge request.
|
||||
- [Fast-forward merge requests](../methods/_index.md#fast-forward-merge):
|
||||
For a linear Git history and a way to accept merge requests without creating merge commits
|
||||
- [Find the merge request that introduced a change](../versions.md):
|
||||
@ -363,5 +323,7 @@ Merge requests are related to these features:
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Compare changes in merge requests](../changes.md)
|
||||
- [Compare branches](../../repository/branches/_index.md#compare-branches)
|
||||
- [Merge methods](../methods/_index.md)
|
||||
- [Draft Notes API](../../../../api/draft_notes.md)
|
||||
|
@ -275,6 +275,71 @@ To compare branches in a repository:
|
||||
1. Select **Compare** to show the list of commits, and changed files.
|
||||
1. Optional. To reverse the **Source** and **Target**, select **Swap revisions** ({{< icon name="substitute" >}}).
|
||||
|
||||
### Download branch comparisons
|
||||
|
||||
{{< history >}}
|
||||
|
||||
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217206) in GitLab 18.3.
|
||||
|
||||
{{< /history >}}
|
||||
|
||||
You can download the comparison between branches as a diff or patch file for use outside of GitLab.
|
||||
|
||||
#### As a diff
|
||||
|
||||
To download the branch comparison as a diff, add `format=diff` to the compare URL:
|
||||
|
||||
- If the URL has no query parameters, append `?format=diff`:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.example.com/my-group/my-project/-/compare/main...feature-branch?format=diff
|
||||
```
|
||||
|
||||
- If the URL already has query parameters, append `&format=diff`:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.example.com/my-group/my-project/-/compare/main...feature-branch?from_project_id=2&format=diff
|
||||
```
|
||||
|
||||
To download and apply the diff:
|
||||
|
||||
```shell
|
||||
curl "https://gitlab.example.com/my-group/my-project/-/compare/main...feature-branch?format=diff" | git apply
|
||||
```
|
||||
|
||||
#### As a patch file
|
||||
|
||||
To download the branch comparison as a patch file, add `format=patch` to the compare URL:
|
||||
|
||||
- If the URL has no query parameters, append `?format=patch`:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.example.com/my-group/my-project/-/compare/main...feature-branch?format=patch
|
||||
```
|
||||
|
||||
- If the URL already has query parameters, append `&format=patch`:
|
||||
|
||||
```plaintext
|
||||
https://gitlab.example.com/my-group/my-project/-/compare/main...feature-branch?from_project_id=2&format=patch
|
||||
```
|
||||
|
||||
To download and apply the patch using [`git am`](https://git-scm.com/docs/git-am):
|
||||
|
||||
```shell
|
||||
# Download and preview the patch
|
||||
curl "https://gitlab.example.com/my-group/my-project/-/compare/main...feature-branch?format=patch" > changes.patch
|
||||
git apply --check changes.patch
|
||||
|
||||
# Apply the patch
|
||||
git am changes.patch
|
||||
```
|
||||
|
||||
You can also download and apply the patch in a single command:
|
||||
|
||||
```shell
|
||||
curl "https://gitlab.example.com/my-group/my-project/-/compare/main...feature-branch?format=patch" | git am
|
||||
```
|
||||
|
||||
## Delete merged branches
|
||||
|
||||
Merged branches can be deleted in bulk if they meet all of these criteria:
|
||||
@ -381,6 +446,9 @@ To do this:
|
||||
## Related topics
|
||||
|
||||
- [Protected branches](protected.md)
|
||||
- [Branch rules](branch_rules.md)
|
||||
- [Compare changes in merge requests](../../merge_requests/changes.md)
|
||||
- [Download merge request changes](../../merge_requests/changes.md#download-merge-request-changes)
|
||||
- [Branches API](../../../../api/branches.md)
|
||||
- [Protected Branches API](../../../../api/protected_branches.md)
|
||||
- [Getting started with Git](../../../../topics/git/_index.md)
|
||||
|
@ -86,7 +86,7 @@ Prerequisites:
|
||||
To allow a cluster agent for workspaces in a group:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. On the left sidebar, select **Settings > Workspaces**.
|
||||
1. On the left sidebar, select **Settings** > **Workspaces**.
|
||||
1. In the **Group agents** section, select the **All agents** tab.
|
||||
1. From the list of available agents, find the agent with status **Blocked**, and select **Allow**.
|
||||
1. On the confirmation dialog, select **Allow agent**.
|
||||
@ -103,7 +103,7 @@ Prerequisites:
|
||||
To remove an allowed cluster agent from a group:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. On the left sidebar, select **Settings > Workspaces**.
|
||||
1. On the left sidebar, select **Settings** > **Workspaces**.
|
||||
1. In the **Group agents** section, select the **Allowed agents** tab.
|
||||
1. From the list of allowed agents, find the agent you want to remove, and select **Block**.
|
||||
1. On the confirmation dialog, select **Block agent**.
|
||||
|
@ -135,7 +135,7 @@ Only one agent is required. You can create workspaces from all projects in a gro
|
||||
To allow your GitLab agent for Kubernetes in a group and make it available to all projects in that group:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. Select **Settings > Workspaces**.
|
||||
1. Select **Settings** > **Workspaces**.
|
||||
1. In the **Group agents** section, select the **All agents** tab.
|
||||
1. For the GitLab agent for Kubernetes, select **Allow**.
|
||||
1. On the confirmation dialog, select **Allow agent**.
|
||||
|
@ -156,7 +156,7 @@ To create a token for the agent:
|
||||
|
||||
1. Go to your group.
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Operate > Kubernetes clusters**.
|
||||
1. Select **Operate** > **Kubernetes clusters**.
|
||||
1. Select **Connect a cluster**.
|
||||
1. Enter a name for your agent and save for later use. For example, `gitlab-workspaces-agentk-eks`.
|
||||
1. Select **Create and register**.
|
||||
@ -194,7 +194,7 @@ pipeline can run.
|
||||
To configure CI/CD variables:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your project.
|
||||
1. Select **Settings > CI/CD**.
|
||||
1. Select **Settings** > **CI/CD**.
|
||||
1. Expand **Variables**.
|
||||
1. In the **Project variables** section, add the following required variables:
|
||||
|
||||
@ -271,7 +271,7 @@ resources in AWS.
|
||||
To run the pipeline:
|
||||
|
||||
1. Create a new pipeline in your GitLab project:
|
||||
1. On the left sidebar, select **Build > Pipelines**.
|
||||
1. On the left sidebar, select **Build** > **Pipelines**.
|
||||
1. Select **New pipeline** and select **New pipeline** again to confirm.
|
||||
1. Verify the `plan` job succeeds, then manually trigger the `apply` job.
|
||||
|
||||
@ -309,7 +309,7 @@ Next, you'll authorize the GitLab agent for Kubernetes to connect to your GitLab
|
||||
To authorize the agent:
|
||||
|
||||
1. On the left sidebar, select **Search or go to** and find your group.
|
||||
1. Select **Settings > Workspaces**.
|
||||
1. Select **Settings** > **Workspaces**.
|
||||
1. In the **Group agents** section, select the **All agents** tab.
|
||||
1. From the list of available agents, find the agent with status **Blocked**, and select **Allow**.
|
||||
1. On the confirmation dialog, select **Allow agent**.
|
||||
|
@ -144,6 +144,10 @@ module API
|
||||
access_token.user || raise(UnauthorizedError)
|
||||
end
|
||||
|
||||
def set_agent_on_context(agent:)
|
||||
::Gitlab::ApplicationContext.push(kubernetes_agent: agent)
|
||||
end
|
||||
|
||||
def access_token
|
||||
return unless params[:access_key].present?
|
||||
|
||||
|
@ -36,7 +36,8 @@ module Gitlab
|
||||
:auth_fail_token_id,
|
||||
:auth_fail_requested_scopes,
|
||||
:http_router_rule_action,
|
||||
:http_router_rule_type
|
||||
:http_router_rule_type,
|
||||
:kubernetes_agent_id
|
||||
].freeze
|
||||
private_constant :KNOWN_KEYS
|
||||
|
||||
@ -73,7 +74,8 @@ module Gitlab
|
||||
Attribute.new(:auth_fail_token_id, String),
|
||||
Attribute.new(:auth_fail_requested_scopes, String),
|
||||
Attribute.new(:http_router_rule_action, String),
|
||||
Attribute.new(:http_router_rule_type, String)
|
||||
Attribute.new(:http_router_rule_type, String),
|
||||
Attribute.new(:kubernetes_agent, ::Clusters::Agent)
|
||||
].freeze
|
||||
private_constant :APPLICATION_ATTRIBUTES
|
||||
|
||||
@ -163,6 +165,7 @@ module Gitlab
|
||||
hash[:pipeline_id] = -> { job&.pipeline_id } if set_values.include?(:job)
|
||||
hash[:job_id] = -> { job&.id } if set_values.include?(:job)
|
||||
hash[:artifact_size] = -> { artifact&.size } if set_values.include?(:artifact)
|
||||
hash[:kubernetes_agent_id] = -> { kubernetes_agent&.id } if set_values.include?(:kubernetes_agent)
|
||||
end
|
||||
end
|
||||
# rubocop: enable Metrics/CyclomaticComplexity
|
||||
|
@ -22,7 +22,7 @@ module QA
|
||||
#
|
||||
# @return [String]
|
||||
def sandbox_name
|
||||
return "gitlab-e2e-sandbox-group-#{Time.now.wday + 1}" if live_env?
|
||||
return "gitlab-e2e-sandbox-group-#{sandbox_number}" if live_env?
|
||||
|
||||
"e2e-sandbox-#{SecureRandom.hex(6)}"
|
||||
end
|
||||
@ -39,6 +39,23 @@ module QA
|
||||
# There is no case to change gitlab address in the middle of test process so it should be safe to do
|
||||
@live_env = Runtime::Env.running_on_live_env?
|
||||
end
|
||||
|
||||
# Determines the sandbox group number for live environments
|
||||
#
|
||||
# Live environments (like .com) have exactly 8 pre-created sandbox groups
|
||||
# (gitlab-e2e-sandbox-group-1 through gitlab-e2e-sandbox-group-8). This method
|
||||
# maps CI_NODE_INDEX values to the available 1-8 range using modulo arithmetic
|
||||
# to handle parallel jobs with more than 8 nodes. When CI_NODE_INDEX is not
|
||||
# set, returns a memoized random number between 1-8.
|
||||
#
|
||||
# @return [Integer] A number between 1 and 8 inclusive, corresponding to available sandbox groups
|
||||
# @example
|
||||
# ENV['CI_NODE_INDEX'] = '1' # returns 1 (gitlab-e2e-sandbox-group-1)
|
||||
# ENV['CI_NODE_INDEX'] = '9' # returns 1 (wraps to gitlab-e2e-sandbox-group-1)
|
||||
# ENV['CI_NODE_INDEX'] = nil # returns random 1-8 (memoized)
|
||||
def sandbox_number
|
||||
ENV['CI_NODE_INDEX'] ? ((ENV['CI_NODE_INDEX'].to_i - 1) % 8) + 1 : @random_sandbox_id ||= rand(1..8)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -9,6 +9,22 @@ RSpec.describe QA::Runtime::Namespace do
|
||||
described_class.instance_variable_set(:@time, nil)
|
||||
end
|
||||
|
||||
shared_examples "sandbox naming for live environments" do
|
||||
context "when the job does not use parallel" do
|
||||
it "returns a random sandbox name 1-8" do
|
||||
expect(described_class.sandbox_name).to match(%r{gitlab-e2e-sandbox-group-[1-8]})
|
||||
end
|
||||
end
|
||||
|
||||
context "when the job uses parallel" do
|
||||
it "returns sandbox name based on CI_NODE_INDEX" do
|
||||
stub_env('CI_NODE_INDEX', '3')
|
||||
|
||||
expect(described_class.sandbox_name).to match('gitlab-e2e-sandbox-group-3')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.group_name' do
|
||||
it "returns unique name with predefined pattern" do
|
||||
expect(described_class.group_name).to match(/e2e-test-#{time.strftime('%Y-%m-%d-%H-%M-%S')}-[a-f0-9]{16}/)
|
||||
@ -30,17 +46,13 @@ RSpec.describe QA::Runtime::Namespace do
|
||||
context "when running on .com environment" do
|
||||
let(:dot_com) { true }
|
||||
|
||||
it "returns day specific sandbox name" do
|
||||
expect(described_class.sandbox_name).to match(%r{gitlab-e2e-sandbox-group-#{time.wday + 1}})
|
||||
end
|
||||
it_behaves_like "sandbox naming for live environments"
|
||||
end
|
||||
|
||||
context "when running on release environment" do
|
||||
let(:release) { true }
|
||||
|
||||
it "returns day specific sandbox name" do
|
||||
expect(described_class.sandbox_name).to match(%r{gitlab-e2e-sandbox-group-#{time.wday + 1}})
|
||||
end
|
||||
it_behaves_like "sandbox naming for live environments"
|
||||
end
|
||||
|
||||
context "when running on ephemeral environment" do
|
||||
|
@ -44,11 +44,22 @@ RSpec.describe 'CI Lint', :js, feature_category: :pipeline_composition do
|
||||
end
|
||||
|
||||
context 'YAML is incorrect' do
|
||||
let(:yaml_content) { 'value: cannot have :' }
|
||||
let(:yaml_content) do
|
||||
<<~YAML.strip
|
||||
invalid: yaml content
|
||||
that: has
|
||||
multiple: lines
|
||||
value: cannot have :
|
||||
more: content
|
||||
YAML
|
||||
end
|
||||
|
||||
it 'displays information about an error' do
|
||||
expect(page).to have_content('Status: Syntax is incorrect')
|
||||
expect(page).to have_selector(content_selector, text: yaml_content)
|
||||
|
||||
expect(page).to have_selector(content_selector)
|
||||
rendered_content = find(content_selector).text
|
||||
expect(rendered_content.strip).to eq(yaml_content.strip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,22 +1,28 @@
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import CiLint from '~/ci/ci_lint/components/ci_lint.vue';
|
||||
import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
|
||||
import lintCIMutation from '~/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
|
||||
import ciLintMutation from '~/ci/pipeline_editor/graphql/mutations/ci_lint.mutation.graphql';
|
||||
import SourceEditor from '~/vue_shared/components/source_editor.vue';
|
||||
import { mockLintDataValid } from '../mock_data';
|
||||
import { mockCiLintMutationResponse } from '../../pipeline_editor/mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
describe('CI Lint', () => {
|
||||
let wrapper;
|
||||
let mockCiLintData;
|
||||
|
||||
const endpoint = '/namespace/project/-/ci/lint';
|
||||
const content =
|
||||
"test_job:\n stage: build\n script: echo 'Building'\n only:\n - web\n - chat\n - pushes\n allow_failure: true ";
|
||||
const mockMutate = jest.fn().mockResolvedValue(mockLintDataValid);
|
||||
|
||||
const createComponent = () => {
|
||||
const handlers = [[ciLintMutation, mockCiLintData]];
|
||||
const mockApollo = createMockApollo(handlers);
|
||||
|
||||
wrapper = shallowMount(CiLint, {
|
||||
data() {
|
||||
return {
|
||||
@ -24,15 +30,11 @@ describe('CI Lint', () => {
|
||||
};
|
||||
},
|
||||
propsData: {
|
||||
endpoint,
|
||||
pipelineSimulationHelpPagePath: '/help/ci/lint#pipeline-simulation',
|
||||
lintHelpPagePath: '/help/ci/lint#anchor',
|
||||
projectFullPath: '/path/to/project',
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
mutate: mockMutate,
|
||||
},
|
||||
},
|
||||
apolloProvider: mockApollo,
|
||||
});
|
||||
};
|
||||
|
||||
@ -44,37 +46,40 @@ describe('CI Lint', () => {
|
||||
const findDryRunToggle = () => wrapper.find('[data-testid="ci-lint-dryrun"]');
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockMutate.mockClear();
|
||||
mockCiLintData = jest.fn();
|
||||
});
|
||||
|
||||
it('displays the editor', () => {
|
||||
createComponent();
|
||||
expect(findEditor().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('validate action calls mutation correctly', () => {
|
||||
createComponent();
|
||||
findValidateBtn().vm.$emit('click');
|
||||
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: lintCIMutation,
|
||||
variables: { content, dry: false, endpoint },
|
||||
expect(mockCiLintData).toHaveBeenCalledWith({
|
||||
projectPath: '/path/to/project',
|
||||
content,
|
||||
dryRun: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('validate action calls mutation with dry run', () => {
|
||||
createComponent();
|
||||
findDryRunToggle().vm.$emit('input', true);
|
||||
findValidateBtn().vm.$emit('click');
|
||||
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: lintCIMutation,
|
||||
variables: { content, dry: true, endpoint },
|
||||
expect(mockCiLintData).toHaveBeenCalledWith({
|
||||
projectPath: '/path/to/project',
|
||||
content,
|
||||
dryRun: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('validation displays results', async () => {
|
||||
mockCiLintData.mockResolvedValue(mockCiLintMutationResponse);
|
||||
createComponent();
|
||||
findValidateBtn().vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
@ -88,7 +93,8 @@ describe('CI Lint', () => {
|
||||
});
|
||||
|
||||
it('validation displays error', async () => {
|
||||
mockMutate.mockRejectedValue('Error!');
|
||||
mockCiLintData.mockRejectedValueOnce(new Error('Error!'));
|
||||
createComponent();
|
||||
|
||||
findValidateBtn().vm.$emit('click');
|
||||
|
||||
@ -99,11 +105,12 @@ describe('CI Lint', () => {
|
||||
await waitForPromises();
|
||||
|
||||
expect(findCiLintResults().exists()).toBe(false);
|
||||
expect(findAlert().text()).toBe('Error!');
|
||||
expect(findAlert().text()).toBe('Error: Error!');
|
||||
expect(findValidateBtn().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('content is cleared on clear action', async () => {
|
||||
createComponent();
|
||||
expect(findEditor().props('value')).toBe(content);
|
||||
|
||||
await findClearBtn().vm.$emit('click');
|
||||
|
@ -1,42 +1,47 @@
|
||||
import { mockJobs } from 'jest/ci/pipeline_editor/mock_data';
|
||||
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
|
||||
|
||||
export const mockLintDataError = {
|
||||
data: {
|
||||
lintCI: {
|
||||
errors: ['Error message'],
|
||||
warnings: ['Warning message'],
|
||||
valid: false,
|
||||
jobs: mockJobs.map((j) => {
|
||||
const job = { ...j, tags: j.tagList };
|
||||
delete job.tagList;
|
||||
return job;
|
||||
}),
|
||||
},
|
||||
export const mockCiLintJobs = [
|
||||
{
|
||||
beforeScript: [],
|
||||
afterScript: [],
|
||||
environment: null,
|
||||
allowFailure: false,
|
||||
tags: [],
|
||||
when: 'on_success',
|
||||
only: { refs: ['branches', 'tags'], __typename: 'CiJobLimitType' },
|
||||
except: null,
|
||||
needs: [{ name: 'test', __typename: 'CiConfigNeed' }],
|
||||
__typename: 'CiConfigJobV2',
|
||||
name: 'job_test_1',
|
||||
script: ['echo "test 1"'],
|
||||
stage: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockLintDataValid = {
|
||||
data: {
|
||||
lintCI: {
|
||||
errors: [],
|
||||
warnings: [],
|
||||
valid: true,
|
||||
jobs: mockJobs.map((j) => {
|
||||
const job = { ...j, tags: j.tagList };
|
||||
delete job.tagList;
|
||||
return job;
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: 'job_test_2',
|
||||
script: ['echo "test 2"'],
|
||||
stage: 'test',
|
||||
beforeScript: [],
|
||||
afterScript: [],
|
||||
environment: null,
|
||||
allowFailure: false,
|
||||
tags: [],
|
||||
when: 'on_success',
|
||||
only: { refs: ['branches', 'tags'], __typename: 'CiJobLimitType' },
|
||||
except: null,
|
||||
needs: [{ name: 'test', __typename: 'CiConfigNeed' }],
|
||||
__typename: 'CiConfigJobV2',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockLintDataErrorRest = {
|
||||
...mockLintDataError.data.lintCI,
|
||||
jobs: mockJobs.map((j) => convertObjectPropsToSnakeCase(j)),
|
||||
};
|
||||
|
||||
export const mockLintDataValidRest = {
|
||||
...mockLintDataValid.data.lintCI,
|
||||
jobs: mockJobs.map((j) => convertObjectPropsToSnakeCase(j)),
|
||||
};
|
||||
{
|
||||
name: 'job_build',
|
||||
script: ['echo "build"'],
|
||||
stage: 'build',
|
||||
beforeScript: [],
|
||||
afterScript: [],
|
||||
environment: null,
|
||||
allowFailure: false,
|
||||
tags: [],
|
||||
when: 'on_success',
|
||||
only: { refs: ['branches', 'tags'], __typename: 'CiJobLimitType' },
|
||||
except: null,
|
||||
needs: [{ name: 'test', __typename: 'CiConfigNeed' }],
|
||||
__typename: 'CiConfigJobV2',
|
||||
},
|
||||
];
|
||||
|
@ -1,43 +1,38 @@
|
||||
import Vue from 'vue';
|
||||
import { GlAlert, GlDisclosureDropdown, GlEmptyState, GlLoadingIcon, GlPopover } from '@gitlab/ui';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
|
||||
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
|
||||
import HelpIcon from '~/vue_shared/components/help_icon/help_icon.vue';
|
||||
import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
|
||||
import CiValidate, { i18n } from '~/ci/pipeline_editor/components/validate/ci_validate.vue';
|
||||
import ValidatePipelinePopover from '~/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
|
||||
import getBlobContent from '~/ci/pipeline_editor/graphql/queries/blob_content.query.graphql';
|
||||
import ciLintMutation from '~/ci/pipeline_editor/graphql/mutations/ci_lint.mutation.graphql';
|
||||
import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
|
||||
import { pipelineEditorTrackingOptions } from '~/ci/pipeline_editor/constants';
|
||||
import {
|
||||
mockBlobContentQueryResponse,
|
||||
mockCiLintMutationResponse,
|
||||
ciLintErrorResponse,
|
||||
mockCiLintPath,
|
||||
mockCiYml,
|
||||
mockCurrentBranchResponse,
|
||||
mockDefaultBranch,
|
||||
mockSimulatePipelineHelpPagePath,
|
||||
} from '../../mock_data';
|
||||
import {
|
||||
mockLintDataError,
|
||||
mockLintDataValid,
|
||||
mockLintDataErrorRest,
|
||||
mockLintDataValidRest,
|
||||
} from '../../../ci_lint/mock_data';
|
||||
|
||||
let mockAxios;
|
||||
import { mockCiLintJobs } from '../../../ci_lint/mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const defaultProvide = {
|
||||
ciConfigPath: '/path/to/ci-config',
|
||||
ciLintPath: mockCiLintPath,
|
||||
currentBranch: 'main',
|
||||
projectFullPath: '/path/to/project',
|
||||
validateTabIllustrationPath: '/path/to/img',
|
||||
simulatePipelineHelpPagePath: mockSimulatePipelineHelpPagePath,
|
||||
@ -46,12 +41,21 @@ const defaultProvide = {
|
||||
describe('Pipeline Editor Validate Tab', () => {
|
||||
let wrapper;
|
||||
let mockBlobContentData;
|
||||
let mockCiLintData;
|
||||
let trackingSpy;
|
||||
|
||||
const createComponent = ({ props, stubs } = {}) => {
|
||||
const handlers = [[getBlobContent, mockBlobContentData]];
|
||||
const handlers = [
|
||||
[getBlobContent, mockBlobContentData],
|
||||
[ciLintMutation, mockCiLintData],
|
||||
];
|
||||
const mockApollo = createMockApollo(handlers, resolvers);
|
||||
|
||||
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||
query: getCurrentBranch,
|
||||
data: mockCurrentBranchResponse,
|
||||
});
|
||||
|
||||
wrapper = shallowMountExtended(CiValidate, {
|
||||
propsData: {
|
||||
ciFileContent: mockCiYml,
|
||||
@ -80,14 +84,8 @@ describe('Pipeline Editor Validate Tab', () => {
|
||||
const findResultsCta = () => wrapper.findByTestId('resimulate-pipeline-button');
|
||||
|
||||
beforeEach(() => {
|
||||
mockAxios = new MockAdapter(axios);
|
||||
mockAxios.onPost(defaultProvide.ciLintPath).reply(HTTP_STATUS_OK, mockLintDataValidRest);
|
||||
|
||||
mockBlobContentData = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockAxios.restore();
|
||||
mockCiLintData = jest.fn();
|
||||
});
|
||||
|
||||
describe('while initial CI content is loading', () => {
|
||||
@ -115,7 +113,9 @@ describe('Pipeline Editor Validate Tab', () => {
|
||||
expect(findPipelineSource().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders enabled CTA without tooltip', () => {
|
||||
it('renders enabled CTA without tooltip', async () => {
|
||||
await waitForPromises();
|
||||
|
||||
expect(findCta().exists()).toBe(true);
|
||||
expect(findCta().props('disabled')).toBe(false);
|
||||
expect(findDisabledCtaTooltip().exists()).toBe(false);
|
||||
@ -162,24 +162,25 @@ describe('Pipeline Editor Validate Tab', () => {
|
||||
expect(findCta().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('calls endpoint with the correct input', async () => {
|
||||
it('calls ciLint mutation with the correct input', async () => {
|
||||
findCta().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockAxios.history.post).toHaveLength(1);
|
||||
expect(mockAxios.history.post[0].data).toBe(
|
||||
JSON.stringify({
|
||||
content: mockCiYml,
|
||||
dry_run: true,
|
||||
}),
|
||||
);
|
||||
expect(mockCiLintData).toHaveBeenCalledWith({
|
||||
projectPath: defaultProvide.projectFullPath,
|
||||
content: mockCiYml,
|
||||
ref: mockDefaultBranch,
|
||||
dryRun: true,
|
||||
});
|
||||
});
|
||||
|
||||
describe('when results are successful', () => {
|
||||
beforeEach(async () => {
|
||||
findCta().vm.$emit('click');
|
||||
mockCiLintData.mockResolvedValue(mockCiLintMutationResponse);
|
||||
await createComponent();
|
||||
|
||||
findCta().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
@ -200,14 +201,15 @@ describe('Pipeline Editor Validate Tab', () => {
|
||||
dryRun: true,
|
||||
hideAlert: true,
|
||||
isValid: true,
|
||||
jobs: mockLintDataValid.data.lintCI.jobs,
|
||||
jobs: mockCiLintJobs,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when results have errors', () => {
|
||||
beforeEach(async () => {
|
||||
mockAxios.onPost(defaultProvide.ciLintPath).reply(HTTP_STATUS_OK, mockLintDataErrorRest);
|
||||
mockCiLintData.mockResolvedValue(ciLintErrorResponse);
|
||||
|
||||
findCta().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
@ -225,8 +227,8 @@ describe('Pipeline Editor Validate Tab', () => {
|
||||
dryRun: true,
|
||||
hideAlert: true,
|
||||
isValid: false,
|
||||
errors: mockLintDataError.data.lintCI.errors,
|
||||
warnings: mockLintDataError.data.lintCI.warnings,
|
||||
errors: ciLintErrorResponse.data.ciLint.config.errors,
|
||||
warnings: ciLintErrorResponse.data.ciLint.config.warnings,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -234,6 +236,7 @@ describe('Pipeline Editor Validate Tab', () => {
|
||||
|
||||
describe('when CI content has changed after a simulation', () => {
|
||||
beforeEach(async () => {
|
||||
mockCiLintData.mockResolvedValue(mockCiLintMutationResponse);
|
||||
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
|
||||
await createComponent();
|
||||
|
||||
@ -272,13 +275,13 @@ describe('Pipeline Editor Validate Tab', () => {
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(mockAxios.history.post).toHaveLength(2);
|
||||
expect(mockAxios.history.post[1].data).toBe(
|
||||
JSON.stringify({
|
||||
content: newContent,
|
||||
dry_run: true,
|
||||
}),
|
||||
);
|
||||
expect(mockCiLintData).toHaveBeenCalledTimes(2);
|
||||
expect(mockCiLintData).toHaveBeenCalledWith({
|
||||
content: 'new yaml content',
|
||||
dryRun: true,
|
||||
projectPath: '/path/to/project',
|
||||
ref: mockDefaultBranch,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -1,73 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`~/ci/pipeline_editor/graphql/resolvers Mutation lintCI lint data is as expected 1`] = `
|
||||
{
|
||||
"__typename": "CiLintContent",
|
||||
"errors": [],
|
||||
"jobs": [
|
||||
{
|
||||
"__typename": "CiLintJob",
|
||||
"afterScript": [
|
||||
"echo 'after script 1",
|
||||
],
|
||||
"allowFailure": false,
|
||||
"beforeScript": [
|
||||
"echo 'before script 1'",
|
||||
],
|
||||
"environment": "prd",
|
||||
"except": {
|
||||
"refs": [
|
||||
"main@gitlab-org/gitlab",
|
||||
"/^release/.*$/@gitlab-org/gitlab",
|
||||
],
|
||||
},
|
||||
"name": "job_1",
|
||||
"only": null,
|
||||
"script": [
|
||||
"echo 'script 1'",
|
||||
],
|
||||
"stage": "test",
|
||||
"tags": [
|
||||
"tag 1",
|
||||
],
|
||||
"when": "on_success",
|
||||
},
|
||||
{
|
||||
"__typename": "CiLintJob",
|
||||
"afterScript": [
|
||||
"echo 'after script 2",
|
||||
],
|
||||
"allowFailure": true,
|
||||
"beforeScript": [
|
||||
"echo 'before script 2'",
|
||||
],
|
||||
"environment": "stg",
|
||||
"except": {
|
||||
"refs": [
|
||||
"main@gitlab-org/gitlab",
|
||||
"/^release/.*$/@gitlab-org/gitlab",
|
||||
],
|
||||
},
|
||||
"name": "job_2",
|
||||
"only": {
|
||||
"__typename": "CiLintJobOnlyPolicy",
|
||||
"refs": [
|
||||
"web",
|
||||
"chat",
|
||||
"pushes",
|
||||
],
|
||||
},
|
||||
"script": [
|
||||
"echo 'script 2'",
|
||||
],
|
||||
"stage": "test",
|
||||
"tags": [
|
||||
"tag 2",
|
||||
],
|
||||
"when": "on_success",
|
||||
},
|
||||
],
|
||||
"valid": true,
|
||||
"warnings": [],
|
||||
}
|
||||
`;
|
@ -1,52 +0,0 @@
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
|
||||
import { mockLintResponse } from '../mock_data';
|
||||
|
||||
jest.mock('~/api', () => {
|
||||
return {
|
||||
getRawFile: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('~/ci/pipeline_editor/graphql/resolvers', () => {
|
||||
describe('Mutation', () => {
|
||||
describe('lintCI', () => {
|
||||
let mock;
|
||||
let result;
|
||||
|
||||
const endpoint = '/ci/lint';
|
||||
|
||||
beforeEach(async () => {
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onPost(endpoint).reply(HTTP_STATUS_OK, mockLintResponse);
|
||||
|
||||
result = await resolvers.Mutation.lintCI(null, {
|
||||
endpoint,
|
||||
content: 'content',
|
||||
dry_run: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
it('lint data has correct type names', () => {
|
||||
expect(result.__typename).toBe('CiLintContent');
|
||||
|
||||
expect(result.jobs[0].__typename).toBe('CiLintJob');
|
||||
expect(result.jobs[1].__typename).toBe('CiLintJob');
|
||||
|
||||
expect(result.jobs[1].only.__typename).toBe('CiLintJobOnlyPolicy');
|
||||
});
|
||||
/* eslint-enable no-underscore-dangle */
|
||||
|
||||
it('lint data is as expected', () => {
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -225,90 +225,6 @@ export const mockCiLintMutationResponse = {
|
||||
},
|
||||
};
|
||||
|
||||
// Mock result of the graphql query at:
|
||||
// app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.graphql
|
||||
export const mockCiConfigQueryResponse = {
|
||||
data: {
|
||||
ciConfig: {
|
||||
errors: [],
|
||||
includes: mockIncludes,
|
||||
mergedYaml: mockCiYml,
|
||||
status: CI_CONFIG_STATUS_VALID,
|
||||
stages: {
|
||||
__typename: 'CiConfigStageConnection',
|
||||
nodes: [
|
||||
{
|
||||
name: 'test',
|
||||
groups: {
|
||||
nodes: [
|
||||
{
|
||||
id: 'group-1',
|
||||
name: 'job_test_1',
|
||||
size: 1,
|
||||
jobs: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'job_test_1',
|
||||
script: ['echo "test 1"'],
|
||||
...mockJobFields,
|
||||
},
|
||||
],
|
||||
__typename: 'CiConfigJobConnection',
|
||||
},
|
||||
__typename: 'CiConfigGroup',
|
||||
},
|
||||
{
|
||||
id: 'group-2',
|
||||
name: 'job_test_2',
|
||||
size: 1,
|
||||
jobs: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'job_test_2',
|
||||
script: ['echo "test 2"'],
|
||||
...mockJobFields,
|
||||
},
|
||||
],
|
||||
__typename: 'CiConfigJobConnection',
|
||||
},
|
||||
__typename: 'CiConfigGroup',
|
||||
},
|
||||
],
|
||||
__typename: 'CiConfigGroupConnection',
|
||||
},
|
||||
__typename: 'CiConfigStage',
|
||||
},
|
||||
{
|
||||
name: 'build',
|
||||
groups: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'job_build',
|
||||
size: 1,
|
||||
jobs: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'job_build',
|
||||
script: ['echo "build"'],
|
||||
...mockJobFields,
|
||||
},
|
||||
],
|
||||
__typename: 'CiConfigJobConnection',
|
||||
},
|
||||
__typename: 'CiConfigGroup',
|
||||
},
|
||||
],
|
||||
__typename: 'CiConfigGroupConnection',
|
||||
},
|
||||
__typename: 'CiConfigStage',
|
||||
},
|
||||
],
|
||||
},
|
||||
__typename: 'CiConfig',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockMergedConfig = (mergedConfig) => {
|
||||
const { config } = mockCiLintMutationResponse.data.ciLint;
|
||||
return {
|
||||
@ -317,6 +233,24 @@ export const mockMergedConfig = (mergedConfig) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const ciLintErrorResponse = {
|
||||
data: {
|
||||
ciLint: {
|
||||
config: {
|
||||
errors: ['header:spec:inputs:app_target_region config contains unknown keys: required'],
|
||||
warnings: [],
|
||||
includes: null,
|
||||
mergedYaml: null,
|
||||
status: 'INVALID',
|
||||
stages: [],
|
||||
__typename: 'CiConfigV2',
|
||||
},
|
||||
errors: [],
|
||||
__typename: 'CiLintPayload',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockCommitShaResults = {
|
||||
data: {
|
||||
project: {
|
||||
@ -570,3 +504,13 @@ export const mockCommitCreateResponseNewEtag = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockCurrentBranchResponse = {
|
||||
workBranches: {
|
||||
__typename: 'BranchList',
|
||||
current: {
|
||||
__typename: 'WorkBranch',
|
||||
name: mockDefaultBranch,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ describe('formatGroups', () => {
|
||||
expect(formatGroups(graphQLGroups)).toEqual(
|
||||
formatGraphQLGroups(graphQLGroups).map((group) => ({
|
||||
...group,
|
||||
editPath: `${group.relativeWebUrl}/-/edit`,
|
||||
editPath: `${group.webUrl}/-/edit`,
|
||||
avatarLabel: group.name,
|
||||
children: expect.any(Object),
|
||||
})),
|
||||
|
@ -250,6 +250,16 @@ RSpec.describe Gitlab::ApplicationContext, feature_category: :shared do
|
||||
expect(result(context)).to include(organization_id: organization.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using kubernetes agent context' do
|
||||
let_it_be(:cluster_agent) { create(:cluster_agent) }
|
||||
|
||||
it 'sets the kubernetes_agent_id value' do
|
||||
context = described_class.new(kubernetes_agent: cluster_agent)
|
||||
|
||||
expect(result(context)).to include(kubernetes_agent_id: cluster_agent.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#use' do
|
||||
|
@ -2016,7 +2016,6 @@
|
||||
- './ee/spec/services/epic_issues/destroy_service_spec.rb'
|
||||
- './ee/spec/services/epic_issues/list_service_spec.rb'
|
||||
- './ee/spec/services/epic_issues/update_service_spec.rb'
|
||||
- './ee/spec/services/epics/close_service_spec.rb'
|
||||
- './ee/spec/services/epics/descendant_count_service_spec.rb'
|
||||
- './ee/spec/services/epics/epic_links/destroy_service_spec.rb'
|
||||
- './ee/spec/services/epics/epic_links/list_service_spec.rb'
|
||||
|
@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples_for 'service scheduling async deletes' do
|
||||
it 'destroys associated todos asynchronously', :sidekiq_inline do
|
||||
it 'destroys associated todos asynchronously' do
|
||||
expect(worker_class).to receive(:perform_async).with(issuable.id, issuable.class.base_class.name)
|
||||
|
||||
if try(:sync_object).present?
|
||||
@ -11,7 +11,7 @@ RSpec.shared_examples_for 'service scheduling async deletes' do
|
||||
subject.execute(issuable)
|
||||
end
|
||||
|
||||
it 'works inside a transaction', :sidekiq_inline do
|
||||
it 'works inside a transaction' do
|
||||
expect(worker_class).to receive(:perform_async).with(issuable.id, issuable.class.base_class.name)
|
||||
|
||||
if try(:sync_object).present?
|
||||
|
Reference in New Issue
Block a user