Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2fe5ea34a5
commit
dc250651ab
52 changed files with 762 additions and 180 deletions
|
@ -1,7 +1,6 @@
|
|||
import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
|
||||
import { editor as monacoEditor, Uri } from 'monaco-editor';
|
||||
import { defaultEditorOptions } from '~/ide/lib/editor_options';
|
||||
import languages from '~/ide/lib/languages';
|
||||
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
|
||||
import { registerLanguages } from '~/ide/utils';
|
||||
import { joinPaths } from '~/lib/utils/url_utility';
|
||||
import { uuids } from '~/lib/utils/uuids';
|
||||
|
@ -11,7 +10,7 @@ import {
|
|||
EDITOR_READY_EVENT,
|
||||
EDITOR_TYPE_DIFF,
|
||||
} from './constants';
|
||||
import { clearDomElement } from './utils';
|
||||
import { clearDomElement, setupEditorTheme, getBlobLanguage } from './utils';
|
||||
|
||||
export default class SourceEditor {
|
||||
constructor(options = {}) {
|
||||
|
@ -22,26 +21,11 @@ export default class SourceEditor {
|
|||
...options,
|
||||
};
|
||||
|
||||
SourceEditor.setupMonacoTheme();
|
||||
setupEditorTheme();
|
||||
|
||||
registerLanguages(...languages);
|
||||
}
|
||||
|
||||
static setupMonacoTheme() {
|
||||
const themeName = window.gon?.user_color_scheme || DEFAULT_THEME;
|
||||
const theme = themes.find((t) => t.name === themeName);
|
||||
if (theme) monacoEditor.defineTheme(themeName, theme.data);
|
||||
monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
|
||||
}
|
||||
|
||||
static getModelLanguage(path) {
|
||||
const ext = `.${path.split('.').pop()}`;
|
||||
const language = monacoLanguages
|
||||
.getLanguages()
|
||||
.find((lang) => lang.extensions.indexOf(ext) !== -1);
|
||||
return language ? language.id : 'plaintext';
|
||||
}
|
||||
|
||||
static pushToImportsArray(arr, toImport) {
|
||||
arr.push(import(toImport));
|
||||
}
|
||||
|
@ -124,10 +108,7 @@ export default class SourceEditor {
|
|||
return model;
|
||||
}
|
||||
const diffModel = {
|
||||
original: monacoEditor.createModel(
|
||||
blobOriginalContent,
|
||||
SourceEditor.getModelLanguage(model.uri.path),
|
||||
),
|
||||
original: monacoEditor.createModel(blobOriginalContent, getBlobLanguage(model.uri.path)),
|
||||
modified: model,
|
||||
};
|
||||
instance.setModel(diffModel);
|
||||
|
@ -155,7 +136,7 @@ export default class SourceEditor {
|
|||
};
|
||||
|
||||
static instanceUpdateLanguage(inst, path) {
|
||||
const lang = SourceEditor.getModelLanguage(path);
|
||||
const lang = getBlobLanguage(path);
|
||||
const model = inst.getModel();
|
||||
return monacoEditor.setModelLanguage(model, lang);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
|
||||
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
|
||||
|
||||
export const clearDomElement = (el) => {
|
||||
if (!el || !el.firstChild) return;
|
||||
|
||||
|
@ -6,6 +9,22 @@ export const clearDomElement = (el) => {
|
|||
}
|
||||
};
|
||||
|
||||
export default () => ({
|
||||
clearDomElement,
|
||||
});
|
||||
export const setupEditorTheme = () => {
|
||||
const themeName = window.gon?.user_color_scheme || DEFAULT_THEME;
|
||||
const theme = themes.find((t) => t.name === themeName);
|
||||
if (theme) monacoEditor.defineTheme(themeName, theme.data);
|
||||
monacoEditor.setTheme(theme ? themeName : DEFAULT_THEME);
|
||||
};
|
||||
|
||||
export const getBlobLanguage = (path) => {
|
||||
const ext = `.${path.split('.').pop()}`;
|
||||
const language = monacoLanguages
|
||||
.getLanguages()
|
||||
.find((lang) => lang.extensions.indexOf(ext) !== -1);
|
||||
return language ? language.id : 'plaintext';
|
||||
};
|
||||
|
||||
export const setupCodeSnippet = (el) => {
|
||||
monacoEditor.colorizeElement(el);
|
||||
setupEditorTheme();
|
||||
};
|
||||
|
|
|
@ -29,8 +29,10 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
showMetadata() {
|
||||
return [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes(
|
||||
return (
|
||||
[PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes(
|
||||
this.packageEntity.packageType,
|
||||
) && this.packageEntity.metadata
|
||||
);
|
||||
},
|
||||
showNugetMetadata() {
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
<script>
|
||||
/*
|
||||
* The commented part of this component needs to be re-enabled in the refactor process,
|
||||
* See here for more info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64939
|
||||
*/
|
||||
import {
|
||||
GlBadge,
|
||||
GlButton,
|
||||
|
@ -19,10 +15,9 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
|
|||
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||
import { objectToQuery } from '~/lib/utils/url_utility';
|
||||
import { s__, __ } from '~/locale';
|
||||
// import DependencyRow from '~/packages/details/components/dependency_row.vue';
|
||||
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
|
||||
import { packageTypeToTrackCategory } from '~/packages/shared/utils';
|
||||
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
|
||||
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
|
||||
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
|
||||
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
|
||||
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
|
||||
|
@ -61,9 +56,8 @@ export default {
|
|||
GlTabs,
|
||||
GlSprintf,
|
||||
PackageTitle,
|
||||
PackagesListLoader,
|
||||
VersionRow,
|
||||
// DependencyRow,
|
||||
DependencyRow,
|
||||
PackageHistory,
|
||||
AdditionalMetadata,
|
||||
InstallationCommands,
|
||||
|
@ -141,7 +135,7 @@ export default {
|
|||
return this.packageEntity.versions?.nodes?.length > 0;
|
||||
},
|
||||
packageDependencies() {
|
||||
return this.packageEntity.dependency_links || [];
|
||||
return this.packageEntity.dependencyLinks?.nodes || [];
|
||||
},
|
||||
showDependencies() {
|
||||
return this.packageEntity.packageType === PACKAGE_TYPE_NUGET;
|
||||
|
@ -268,8 +262,7 @@ export default {
|
|||
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
|
||||
:svg-path="svgPath"
|
||||
/>
|
||||
|
||||
<div v-else class="packages-app">
|
||||
<div v-else-if="!isLoading" class="packages-app">
|
||||
<package-title :package-entity="packageEntity">
|
||||
<template #delete-button>
|
||||
<gl-button
|
||||
|
@ -303,20 +296,14 @@ export default {
|
|||
/>
|
||||
</gl-tab>
|
||||
|
||||
<gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab">
|
||||
<gl-tab v-if="showDependencies">
|
||||
<template #title>
|
||||
<span>{{ __('Dependencies') }}</span>
|
||||
<gl-badge size="sm" data-testid="dependencies-badge">{{
|
||||
packageDependencies.length
|
||||
}}</gl-badge>
|
||||
<gl-badge size="sm">{{ packageDependencies.length }}</gl-badge>
|
||||
</template>
|
||||
|
||||
<template v-if="packageDependencies.length > 0">
|
||||
<!-- <dependency-row
|
||||
v-for="(dep, index) in packageDependencies"
|
||||
:key="index"
|
||||
:dependency="dep"
|
||||
/> -->
|
||||
<dependency-row v-for="dep in packageDependencies" :key="dep.id" :dependency-link="dep" />
|
||||
</template>
|
||||
|
||||
<p v-else class="gl-mt-3" data-testid="no-dependencies-message">
|
||||
|
@ -325,11 +312,7 @@ export default {
|
|||
</gl-tab>
|
||||
|
||||
<gl-tab :title="__('Other versions')" title-item-class="js-versions-tab">
|
||||
<template v-if="isLoading && !hasVersions">
|
||||
<packages-list-loader />
|
||||
</template>
|
||||
|
||||
<template v-else-if="hasVersions">
|
||||
<template v-if="hasVersions">
|
||||
<version-row v-for="v in packageEntity.versions.nodes" :key="v.id" :package-entity="v" />
|
||||
</template>
|
||||
|
||||
|
|
|
@ -2,14 +2,17 @@
|
|||
export default {
|
||||
name: 'DependencyRow',
|
||||
props: {
|
||||
dependency: {
|
||||
dependencyLink: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showVersion() {
|
||||
return Boolean(this.dependency.version_pattern);
|
||||
return Boolean(this.dependencyLink.dependency?.versionPattern);
|
||||
},
|
||||
showTargetFramework() {
|
||||
return Boolean(this.dependencyLink.metadata?.targetFramework);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -18,10 +21,10 @@ export default {
|
|||
<template>
|
||||
<div class="gl-responsive-table-row">
|
||||
<div class="table-section section-50">
|
||||
<strong class="gl-text-body">{{ dependency.name }}</strong>
|
||||
<span v-if="dependency.target_framework" data-testid="target-framework"
|
||||
>({{ dependency.target_framework }})</span
|
||||
>
|
||||
<strong class="gl-text-body">{{ dependencyLink.dependency.name }}</strong>
|
||||
<span v-if="showTargetFramework" data-testid="target-framework">
|
||||
({{ dependencyLink.metadata.targetFramework }})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
@ -29,7 +32,7 @@ export default {
|
|||
class="table-section section-50 gl-display-flex gl-md-justify-content-end"
|
||||
data-testid="version-pattern"
|
||||
>
|
||||
<span class="gl-text-body">{{ dependency.version_pattern }}</span>
|
||||
<span class="gl-text-body">{{ dependencyLink.dependency.versionPattern }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -16,7 +16,7 @@ query getPackageDetails($id: ID!) {
|
|||
name
|
||||
}
|
||||
}
|
||||
pipelines(first: 3) {
|
||||
pipelines(first: 10) {
|
||||
nodes {
|
||||
ref
|
||||
id
|
||||
|
@ -60,6 +60,23 @@ query getPackageDetails($id: ID!) {
|
|||
}
|
||||
}
|
||||
}
|
||||
dependencyLinks {
|
||||
nodes {
|
||||
id
|
||||
dependency {
|
||||
id
|
||||
name
|
||||
versionPattern
|
||||
}
|
||||
dependencyType
|
||||
metadata {
|
||||
... on NugetDependencyLinkMetadata {
|
||||
id
|
||||
targetFramework
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata {
|
||||
... on ComposerMetadata {
|
||||
targetSha
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
<script>
|
||||
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
title: s__('Terraform|Terraform init command'),
|
||||
explanatoryText: s__(
|
||||
`Terraform|To get access to this terraform state from your local computer, run the following command at the command line. The first line requires a personal access token with API read and write access. %{linkStart}How do I create a personal access token?%{linkEnd}.`,
|
||||
),
|
||||
closeText: __('Close'),
|
||||
copyToClipboardText: __('Copy'),
|
||||
},
|
||||
components: {
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
ModalCopyButton,
|
||||
},
|
||||
inject: ['accessTokensPath', 'terraformApiUrl', 'username'],
|
||||
props: {
|
||||
modalId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
stateName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
closeModalProps() {
|
||||
return {
|
||||
text: this.$options.i18n.closeText,
|
||||
attributes: [],
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getModalInfoCopyStr() {
|
||||
return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
|
||||
terraform init \\
|
||||
-backend-config="address=${this.terraformApiUrl}/${this.stateName}" \\
|
||||
-backend-config="lock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
|
||||
-backend-config="unlock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
|
||||
-backend-config="username=${this.username}" \\
|
||||
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
|
||||
-backend-config="lock_method=POST" \\
|
||||
-backend-config="unlock_method=DELETE" \\
|
||||
-backend-config="retry_wait_min=5"
|
||||
`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal
|
||||
ref="initCommandModal"
|
||||
:modal-id="modalId"
|
||||
:title="$options.i18n.title"
|
||||
:action-cancel="closeModalProps"
|
||||
>
|
||||
<p data-testid="init-command-explanatory-text">
|
||||
<gl-sprintf :message="$options.i18n.explanatoryText">
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="accessTokensPath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
|
||||
<div class="gl-display-flex">
|
||||
<pre class="gl-bg-gray gl-white-space-pre-wrap" data-testid="terraform-init-command">{{
|
||||
getModalInfoCopyStr()
|
||||
}}</pre>
|
||||
<modal-copy-button
|
||||
:title="$options.i18n.copyToClipboardText"
|
||||
:text="getModalInfoCopyStr()"
|
||||
:modal-id="$options.modalId"
|
||||
data-testid="init-command-copy-clipboard"
|
||||
css-classes="gl-align-self-start gl-ml-2"
|
||||
/>
|
||||
</div>
|
||||
</gl-modal>
|
||||
</template>
|
|
@ -8,12 +8,14 @@ import {
|
|||
GlIcon,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import addDataToState from '../graphql/mutations/add_data_to_state.mutation.graphql';
|
||||
import lockState from '../graphql/mutations/lock_state.mutation.graphql';
|
||||
import removeState from '../graphql/mutations/remove_state.mutation.graphql';
|
||||
import unlockState from '../graphql/mutations/unlock_state.mutation.graphql';
|
||||
import InitCommandModal from './init_command_modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -25,6 +27,10 @@ export default {
|
|||
GlIcon,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
InitCommandModal,
|
||||
},
|
||||
directives: {
|
||||
GlModalDirective,
|
||||
},
|
||||
props: {
|
||||
state: {
|
||||
|
@ -36,6 +42,7 @@ export default {
|
|||
return {
|
||||
showRemoveModal: false,
|
||||
removeConfirmText: '',
|
||||
showCommandModal: false,
|
||||
};
|
||||
},
|
||||
i18n: {
|
||||
|
@ -54,6 +61,7 @@ export default {
|
|||
remove: s__('Terraform|Remove state file and versions'),
|
||||
removeSuccessful: s__('Terraform|%{name} successfully removed'),
|
||||
unlock: s__('Terraform|Unlock'),
|
||||
copyCommand: s__('Terraform|Copy Terraform init command'),
|
||||
},
|
||||
computed: {
|
||||
cancelModalProps() {
|
||||
|
@ -74,6 +82,9 @@ export default {
|
|||
attributes: [{ disabled: this.disableModalSubmit }, { variant: 'danger' }],
|
||||
};
|
||||
},
|
||||
commandModalId() {
|
||||
return `init-command-modal-${this.state.name}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hideModal() {
|
||||
|
@ -164,6 +175,9 @@ export default {
|
|||
});
|
||||
});
|
||||
},
|
||||
copyInitCommand() {
|
||||
this.showCommandModal = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -181,6 +195,14 @@ export default {
|
|||
<gl-icon class="gl-mr-0" name="ellipsis_v" />
|
||||
</template>
|
||||
|
||||
<gl-dropdown-item
|
||||
v-gl-modal-directive="commandModalId"
|
||||
data-testid="terraform-state-copy-init-command"
|
||||
@click="copyInitCommand"
|
||||
>
|
||||
{{ $options.i18n.copyCommand }}
|
||||
</gl-dropdown-item>
|
||||
|
||||
<gl-dropdown-item
|
||||
v-if="state.latestVersion"
|
||||
data-testid="terraform-state-download"
|
||||
|
@ -248,5 +270,11 @@ export default {
|
|||
/>
|
||||
</gl-form-group>
|
||||
</gl-modal>
|
||||
|
||||
<init-command-modal
|
||||
v-if="showCommandModal"
|
||||
:modal-id="commandModalId"
|
||||
:state-name="state.name"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -24,11 +24,16 @@ export default () => {
|
|||
},
|
||||
});
|
||||
|
||||
const { emptyStateImage, projectPath } = el.dataset;
|
||||
const { emptyStateImage, projectPath, accessTokensPath, terraformApiUrl, username } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
apolloProvider: new VueApollo({ defaultClient }),
|
||||
provide: {
|
||||
accessTokensPath,
|
||||
terraformApiUrl,
|
||||
username,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(TerraformList, {
|
||||
props: {
|
||||
|
|
|
@ -184,6 +184,21 @@ body.gl-dark {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gl-datepicker-theme {
|
||||
.pika-prev,
|
||||
.pika-next {
|
||||
filter: invert(0.9);
|
||||
}
|
||||
|
||||
.is-selected > .pika-button {
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
:not(.is-selected) > .pika-button:hover {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$border-white-normal: $border-color;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
class GroupMembersFinder < UnionFinder
|
||||
RELATIONS = %i(direct inherited descendants).freeze
|
||||
DEFAULT_RELATIONS = %i(direct inherited).freeze
|
||||
INVALID_RELATION_TYPE_ERROR_MSG = "is not a valid relation type. Valid relation types are #{RELATIONS.join(', ')}."
|
||||
|
||||
RELATIONS_DESCRIPTIONS = {
|
||||
direct: 'Members in the group itself',
|
||||
|
@ -42,6 +43,8 @@ class GroupMembersFinder < UnionFinder
|
|||
attr_reader :user, :group
|
||||
|
||||
def groups_by_relations(include_relations)
|
||||
check_relation_arguments!(include_relations)
|
||||
|
||||
case include_relations.sort
|
||||
when [:inherited]
|
||||
group.ancestors
|
||||
|
@ -86,6 +89,12 @@ class GroupMembersFinder < UnionFinder
|
|||
def members_of_groups(groups)
|
||||
GroupMember.non_request.of_groups(groups)
|
||||
end
|
||||
|
||||
def check_relation_arguments!(include_relations)
|
||||
unless include_relations & RELATIONS == include_relations
|
||||
raise ArgumentError, "#{(include_relations - RELATIONS).first} #{INVALID_RELATION_TYPE_ERROR_MSG}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
GroupMembersFinder.prepend_mod_with('GroupMembersFinder')
|
||||
|
|
|
@ -5,7 +5,10 @@ module Projects::TerraformHelper
|
|||
{
|
||||
empty_state_image: image_path('illustrations/empty-state/empty-serverless-lg.svg'),
|
||||
project_path: project.full_path,
|
||||
terraform_admin: current_user&.can?(:admin_terraform_state, project)
|
||||
terraform_admin: current_user&.can?(:admin_terraform_state, project),
|
||||
access_tokens_path: profile_personal_access_tokens_path,
|
||||
username: current_user&.username,
|
||||
terraform_api_url: "#{Settings.gitlab.url}/api/v4/projects/#{project.id}/terraform/state"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -891,7 +891,7 @@ module Ci
|
|||
end
|
||||
|
||||
def valid_dependency?
|
||||
return false if artifacts_expired?
|
||||
return false if artifacts_expired? && !pipeline.artifacts_locked?
|
||||
return false if erased?
|
||||
|
||||
true
|
||||
|
|
|
@ -274,7 +274,7 @@ class Integration < ApplicationRecord
|
|||
end
|
||||
|
||||
def self.closest_group_integration(type, scope)
|
||||
group_ids = scope.ancestors(hierarchy_order: :asc).select(:id)
|
||||
group_ids = scope.ancestors.select(:id)
|
||||
array = group_ids.to_sql.present? ? "array(#{group_ids.to_sql})" : 'ARRAY[]'
|
||||
|
||||
where(type: type, group_id: group_ids, inherit_from_id: nil)
|
||||
|
|
|
@ -178,10 +178,6 @@ module Namespaces
|
|||
depth_sql = "ABS(#{traversal_ids.count} - array_length(traversal_ids, 1))"
|
||||
skope = skope.select(skope.arel_table[Arel.star], "#{depth_sql} as depth")
|
||||
.order(depth: hierarchy_order)
|
||||
# The SELECT includes an extra depth attribute. We then wrap the SQL
|
||||
# in a standard SELECT to avoid mismatched attribute errors when
|
||||
# trying to chain future ActiveRelation commands.
|
||||
skope = self.class.without_sti_condition.from(skope, self.class.table_name)
|
||||
end
|
||||
|
||||
skope
|
||||
|
|
|
@ -914,9 +914,7 @@ class Project < ApplicationRecord
|
|||
.base_and_ancestors(upto: top, hierarchy_order: hierarchy_order)
|
||||
end
|
||||
|
||||
def ancestors(hierarchy_order: nil)
|
||||
namespace&.self_and_ancestors(hierarchy_order: hierarchy_order)
|
||||
end
|
||||
alias_method :ancestors, :ancestors_upto
|
||||
|
||||
def ancestors_upto_ids(...)
|
||||
ancestors_upto(...).pluck(:id)
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
.row
|
||||
.col-12
|
||||
- if Feature.enabled?(:package_details_apollo)
|
||||
- if Feature.enabled?(:package_details_apollo, default_enabled: :yaml)
|
||||
#js-vue-packages-detail-new{ data: package_details_data(@project, @package) }
|
||||
- else
|
||||
#js-vue-packages-detail{ data: package_details_data(@project, @package, true) }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: ci_pending_builds_maintain_ci_minutes_data
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64443
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332951
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338149
|
||||
milestone: '14.2'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: ci_pending_builds_maintain_shared_runners_data
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64644
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338152
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: ci_queueing_denormalize_shared_runners_information
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66082
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338289
|
||||
milestone: '14.2'
|
||||
type: development
|
||||
group: group::pipeline execution
|
||||
|
|
|
@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66723
|
|||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336750
|
||||
milestone: '14.2'
|
||||
type: development
|
||||
group: group::ecosystem
|
||||
group: group::integrations
|
||||
default_enabled: false
|
||||
|
|
|
@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65298
|
|||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335069
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::ecosystem
|
||||
group: group::integrations
|
||||
default_enabled: false
|
||||
|
|
|
@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60092
|
|||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330628
|
||||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::ecosystem
|
||||
group: group::integrations
|
||||
default_enabled: false
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334786
|
|||
milestone: '14.1'
|
||||
type: development
|
||||
group: group::package
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: report_on_long_redis_durations
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67512
|
||||
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1183
|
||||
milestone: '14.2'
|
||||
type: development
|
||||
group: team::Scalability
|
||||
default_enabled: false
|
|
@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60837
|
|||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329849
|
||||
milestone: '13.12'
|
||||
type: development
|
||||
group: group::ecosystem
|
||||
group: group::integrations
|
||||
default_enabled: false
|
||||
|
|
|
@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23
|
|||
rollout_issue_url:
|
||||
milestone: '11.8'
|
||||
type: ops
|
||||
group: group::ecosystem
|
||||
group: group::integrations
|
||||
default_enabled: false
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 36 KiB |
|
@ -83,6 +83,14 @@ local machine, this is a simple way to get started:
|
|||
-backend-config="retry_wait_min=5"
|
||||
```
|
||||
|
||||
If you already have a GitLab-managed Terraform state, you can use the `terraform init` command
|
||||
with the prepopulated parameters values:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Infrastructure > Terraform**.
|
||||
1. Next to the environment you want to use, select the [Actions menu](#managing-state-files)
|
||||
**{ellipsis_v}** and select **Copy Terraform init command**.
|
||||
|
||||
You can now run `terraform plan` and `terraform apply` as you normally would.
|
||||
|
||||
### Get started using GitLab CI
|
||||
|
@ -222,7 +230,7 @@ An example setup is shown below:
|
|||
```plaintext
|
||||
example_remote_state_address=https://gitlab.com/api/v4/projects/<TARGET-PROJECT-ID>/terraform/state/<TARGET-STATE-NAME>
|
||||
example_username=<GitLab username>
|
||||
example_access_token=<GitLab Personal Acceess Token>
|
||||
example_access_token=<GitLab Personal Access Token>
|
||||
```
|
||||
|
||||
1. Define the data source by adding the following code block in a `.tf` file (such as `data.tf`):
|
||||
|
@ -362,10 +370,8 @@ contains these fields:
|
|||
state file is locked.
|
||||
- **Pipeline**: A link to the most recent pipeline and its status.
|
||||
- **Details**: Information about when the state file was created or changed.
|
||||
- **Actions**: Actions you can take on the state file, including downloading,
|
||||
locking, unlocking, or [removing](#remove-a-state-file) the state file and versions:
|
||||
|
||||
![Terraform state list](img/terraform_list_view_actions_v13_8.png)
|
||||
- **Actions**: Actions you can take on the state file, including copying the `terraform init` command,
|
||||
downloading, locking, unlocking, or [removing](#remove-a-state-file) the state file and versions.
|
||||
|
||||
NOTE:
|
||||
Additional improvements to the
|
||||
|
|
|
@ -6,7 +6,7 @@ module API
|
|||
module JobRequest
|
||||
class Dependency < Grape::Entity
|
||||
expose :id, :name, :token
|
||||
expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.artifacts? }
|
||||
expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.available_artifacts? }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,16 +48,28 @@ module Gitlab
|
|||
commits_by_id = commits.index_by(&:id)
|
||||
|
||||
result = []
|
||||
pending = [newrev]
|
||||
pending = Set[newrev]
|
||||
|
||||
# We go up the parent chain of our newrev and collect all commits which
|
||||
# are new. In case a commit's ID cannot be found in the set of new
|
||||
# commits, then it must already be a preexisting commit.
|
||||
pending.each do |rev|
|
||||
commit = commits_by_id[rev]
|
||||
while pending.any?
|
||||
rev = pending.first
|
||||
pending.delete(rev)
|
||||
|
||||
# Remove the revision from commit candidates such that we don't walk
|
||||
# it multiple times. If the hash doesn't contain the revision, then
|
||||
# we have either already walked the commit or it's not new.
|
||||
commit = commits_by_id.delete(rev)
|
||||
next if commit.nil?
|
||||
|
||||
pending.push(*commit.parent_ids)
|
||||
# Only add the parent ID to the pending set if we actually know its
|
||||
# commit to guards us against readding an ID which we have already
|
||||
# queued up before.
|
||||
commit.parent_ids.each do |parent_id|
|
||||
pending.add(parent_id) if commits_by_id.has_key?(parent_id)
|
||||
end
|
||||
|
||||
result << commit
|
||||
end
|
||||
|
||||
|
|
|
@ -5,8 +5,21 @@ module Gitlab
|
|||
module RedisInterceptor
|
||||
APDEX_EXCLUDE = %w[brpop blpop brpoplpush bzpopmin bzpopmax xread xreadgroup].freeze
|
||||
|
||||
# These are temporary to help with investigating
|
||||
# https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1183
|
||||
DURATION_ERROR_THRESHOLD = 1.25.seconds
|
||||
|
||||
class MysteryRedisDurationError < StandardError
|
||||
attr_reader :backtrace
|
||||
|
||||
def initialize(backtrace)
|
||||
@backtrace = backtrace
|
||||
end
|
||||
end
|
||||
|
||||
def call(*args, &block)
|
||||
start = Gitlab::Metrics::System.monotonic_time # must come first so that 'start' is always defined
|
||||
start_real_time = Time.now
|
||||
instrumentation_class.instance_count_request
|
||||
instrumentation_class.redis_cluster_validate!(args.first)
|
||||
|
||||
|
@ -27,6 +40,13 @@ module Gitlab
|
|||
instrumentation_class.add_duration(duration)
|
||||
instrumentation_class.add_call_details(duration, args)
|
||||
end
|
||||
|
||||
if duration > DURATION_ERROR_THRESHOLD && Feature.enabled?(:report_on_long_redis_durations, default_enabled: :yaml)
|
||||
Gitlab::ErrorTracking.track_exception(MysteryRedisDurationError.new(caller),
|
||||
command: command_from_args(args),
|
||||
duration: duration,
|
||||
timestamp: start_real_time.iso8601(5))
|
||||
end
|
||||
end
|
||||
|
||||
def write(command)
|
||||
|
|
|
@ -11513,7 +11513,7 @@ msgstr ""
|
|||
msgid "DevopsAdoption|Edit subgroups"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Feature adoption is based on usage in the previous calendar month. Last updated: %{timestamp}."
|
||||
msgid "DevopsAdoption|Feature adoption is based on usage in the previous calendar month. Data is updated at the beginning of each month. Last updated: %{timestamp}."
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Fuzz Testing"
|
||||
|
@ -32641,6 +32641,9 @@ msgstr ""
|
|||
msgid "Terraform|Cancel"
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|Copy Terraform init command"
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|Details"
|
||||
msgstr ""
|
||||
|
||||
|
@ -32692,12 +32695,18 @@ msgstr ""
|
|||
msgid "Terraform|States"
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|Terraform init command"
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|The report %{name} failed to generate."
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|The report %{name} was generated in your pipelines."
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|To get access to this terraform state from your local computer, run the following command at the command line. The first line requires a personal access token with API read and write access. %{linkStart}How do I create a personal access token?%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Terraform|To remove the State file and its versions, type %{name} to confirm:"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -52,6 +52,8 @@ RSpec.describe 'Group Packages' do
|
|||
it_behaves_like 'package details link'
|
||||
end
|
||||
|
||||
it_behaves_like 'package details link'
|
||||
|
||||
it 'allows you to navigate to the project page' do
|
||||
find('[data-testid="root-link"]', text: project.name).click
|
||||
|
||||
|
|
|
@ -45,6 +45,8 @@ RSpec.describe 'Packages' do
|
|||
it_behaves_like 'package details link'
|
||||
end
|
||||
|
||||
it_behaves_like 'package details link'
|
||||
|
||||
context 'deleting a package' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:package) { create(:package, project: project) }
|
||||
|
|
|
@ -38,7 +38,7 @@ RSpec.describe 'Terraform', :js do
|
|||
|
||||
it 'displays a table with terraform states' do
|
||||
expect(page).to have_selector(
|
||||
'[data-testid="terraform-states-table-name"]',
|
||||
"[data-testid='terraform-states-table-name']",
|
||||
count: project.terraform_states.size
|
||||
)
|
||||
end
|
||||
|
@ -64,7 +64,7 @@ RSpec.describe 'Terraform', :js do
|
|||
expect(page).to have_content(additional_state.name)
|
||||
|
||||
find("[data-testid='terraform-state-actions-#{additional_state.name}']").click
|
||||
find('[data-testid="terraform-state-remove"]').click
|
||||
find("[data-testid='terraform-state-remove']").click
|
||||
fill_in "terraform-state-remove-input-#{additional_state.name}", with: additional_state.name
|
||||
click_button 'Remove'
|
||||
|
||||
|
@ -72,6 +72,21 @@ RSpec.describe 'Terraform', :js do
|
|||
expect { additional_state.reload }.to raise_error ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
|
||||
context 'when clicking on copy Terraform init command' do
|
||||
it 'shows the modal with the init command' do
|
||||
visit project_terraform_index_path(project)
|
||||
|
||||
expect(page).to have_content(terraform_state.name)
|
||||
|
||||
page.within("[data-testid='terraform-state-actions-#{terraform_state.name}']") do
|
||||
click_button class: 'gl-dropdown-toggle'
|
||||
click_button 'Copy Terraform init command'
|
||||
end
|
||||
|
||||
expect(page).to have_content("To get access to this terraform state from your local computer, run the following command at the command line.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -87,11 +102,11 @@ RSpec.describe 'Terraform', :js do
|
|||
context 'when user visits the index page' do
|
||||
it 'displays a table without an action dropdown', :aggregate_failures do
|
||||
expect(page).to have_selector(
|
||||
'[data-testid="terraform-states-table-name"]',
|
||||
"[data-testid='terraform-states-table-name']",
|
||||
count: project.terraform_states.size
|
||||
)
|
||||
|
||||
expect(page).not_to have_selector('[data-testid*="terraform-state-actions"]')
|
||||
expect(page).not_to have_selector("[data-testid*='terraform-state-actions']")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,6 +38,12 @@ RSpec.describe GroupMembersFinder, '#execute' do
|
|||
}
|
||||
end
|
||||
|
||||
it 'raises an error if a non-supported relation type is used' do
|
||||
expect do
|
||||
described_class.new(group).execute(include_relations: [:direct, :invalid_relation_type])
|
||||
end.to raise_error(ArgumentError, "invalid_relation_type is not a valid relation type. Valid relation types are direct, inherited, descendants.")
|
||||
end
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:subject_relations, :subject_group, :expected_members) do
|
||||
|
|
84
spec/frontend/editor/utils_spec.js
Normal file
84
spec/frontend/editor/utils_spec.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { editor as monacoEditor } from 'monaco-editor';
|
||||
import * as utils from '~/editor/utils';
|
||||
import { DEFAULT_THEME } from '~/ide/lib/themes';
|
||||
|
||||
describe('Source Editor utils', () => {
|
||||
let el;
|
||||
|
||||
const stubUserColorScheme = (value) => {
|
||||
if (window.gon == null) {
|
||||
window.gon = {};
|
||||
}
|
||||
window.gon.user_color_scheme = value;
|
||||
};
|
||||
|
||||
describe('clearDomElement', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures('<div id="foo"><div id="bar">Foo</div></div>');
|
||||
el = document.getElementById('foo');
|
||||
});
|
||||
|
||||
it('removes all child nodes from an element', () => {
|
||||
expect(el.children.length).toBe(1);
|
||||
utils.clearDomElement(el);
|
||||
expect(el.children.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupEditorTheme', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(monacoEditor, 'defineTheme').mockImplementation();
|
||||
jest.spyOn(monacoEditor, 'setTheme').mockImplementation();
|
||||
});
|
||||
|
||||
it.each`
|
||||
themeName | expectedThemeName
|
||||
${'solarized-light'} | ${'solarized-light'}
|
||||
${DEFAULT_THEME} | ${DEFAULT_THEME}
|
||||
${'non-existent'} | ${DEFAULT_THEME}
|
||||
`(
|
||||
'sets the $expectedThemeName theme when $themeName is set in the user preference',
|
||||
({ themeName, expectedThemeName }) => {
|
||||
stubUserColorScheme(themeName);
|
||||
utils.setupEditorTheme();
|
||||
|
||||
expect(monacoEditor.setTheme).toHaveBeenCalledWith(expectedThemeName);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('getBlobLanguage', () => {
|
||||
it.each`
|
||||
path | expectedLanguage
|
||||
${'foo.js'} | ${'javascript'}
|
||||
${'foo.js.rb'} | ${'ruby'}
|
||||
${'foo.bar'} | ${'plaintext'}
|
||||
`(
|
||||
'sets the $expectedThemeName theme when $themeName is set in the user preference',
|
||||
({ path, expectedLanguage }) => {
|
||||
const language = utils.getBlobLanguage(path);
|
||||
|
||||
expect(language).toEqual(expectedLanguage);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('setupCodeSnipet', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(monacoEditor, 'colorizeElement').mockImplementation();
|
||||
jest.spyOn(monacoEditor, 'setTheme').mockImplementation();
|
||||
setFixtures('<pre id="foo"></pre>');
|
||||
el = document.getElementById('foo');
|
||||
});
|
||||
|
||||
it('colorizes the element and applies the preference theme', () => {
|
||||
expect(monacoEditor.colorizeElement).not.toHaveBeenCalled();
|
||||
expect(monacoEditor.setTheme).not.toHaveBeenCalled();
|
||||
|
||||
utils.setupCodeSnippet(el);
|
||||
|
||||
expect(monacoEditor.colorizeElement).toHaveBeenCalledWith(el);
|
||||
expect(monacoEditor.setTheme).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,13 +10,15 @@ exports[`DependencyRow renders full dependency 1`] = `
|
|||
<strong
|
||||
class="gl-text-body"
|
||||
>
|
||||
Test.Dependency
|
||||
Ninject.Extensions.Factory
|
||||
</strong>
|
||||
|
||||
<span
|
||||
data-testid="target-framework"
|
||||
>
|
||||
(.NETStandard2.0)
|
||||
|
||||
(.NETCoreApp3.1)
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -27,7 +29,7 @@ exports[`DependencyRow renders full dependency 1`] = `
|
|||
<span
|
||||
class="gl-text-body"
|
||||
>
|
||||
2.3.7
|
||||
3.3.2
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlEmptyState } from '@gitlab/ui';
|
||||
import { GlEmptyState, GlBadge, GlTabs, GlTab } from '@gitlab/ui';
|
||||
import { createLocalVue } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
|
@ -10,6 +10,7 @@ import createFlash from '~/flash';
|
|||
|
||||
import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
|
||||
import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
|
||||
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
|
||||
import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue';
|
||||
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
|
||||
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
PACKAGE_TYPE_COMPOSER,
|
||||
DELETE_PACKAGE_FILE_SUCCESS_MESSAGE,
|
||||
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
|
||||
PACKAGE_TYPE_NUGET,
|
||||
} from '~/packages_and_registries/package_registry/constants';
|
||||
|
||||
import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
|
||||
|
@ -30,6 +32,7 @@ import {
|
|||
packageDetailsQuery,
|
||||
packageData,
|
||||
packageVersions,
|
||||
dependencyLinks,
|
||||
emptyPackageDetailsQuery,
|
||||
packageDestroyMutation,
|
||||
packageDestroyMutationError,
|
||||
|
@ -85,6 +88,8 @@ describe('PackagesApp', () => {
|
|||
show: jest.fn(),
|
||||
},
|
||||
},
|
||||
GlTabs,
|
||||
GlTab,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -100,6 +105,9 @@ describe('PackagesApp', () => {
|
|||
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
|
||||
const findVersionRows = () => wrapper.findAllComponents(VersionRow);
|
||||
const noVersionsMessage = () => wrapper.findByTestId('no-versions-message');
|
||||
const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
|
||||
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
|
||||
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -401,4 +409,43 @@ describe('PackagesApp', () => {
|
|||
expect(noVersionsMessage().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('dependency links', () => {
|
||||
it('does not show the dependency links for a non nuget package', async () => {
|
||||
createComponent();
|
||||
|
||||
expect(findDependenciesCountBadge().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the dependencies tab with 0 count when a nuget package with no dependencies', async () => {
|
||||
createComponent({
|
||||
resolver: jest.fn().mockResolvedValue(
|
||||
packageDetailsQuery({
|
||||
packageType: PACKAGE_TYPE_NUGET,
|
||||
dependencyLinks: { nodes: [] },
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findDependenciesCountBadge().exists()).toBe(true);
|
||||
expect(findDependenciesCountBadge().text()).toBe('0');
|
||||
expect(findNoDependenciesMessage().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the correct number of dependency rows for a nuget package', async () => {
|
||||
createComponent({
|
||||
resolver: jest.fn().mockResolvedValue(
|
||||
packageDetailsQuery({
|
||||
packageType: PACKAGE_TYPE_NUGET,
|
||||
}),
|
||||
),
|
||||
});
|
||||
await waitForPromises();
|
||||
|
||||
expect(findDependenciesCountBadge().exists()).toBe(true);
|
||||
expect(findDependenciesCountBadge().text()).toBe(dependencyLinks().length.toString());
|
||||
expect(findDependencyRows()).toHaveLength(dependencyLinks().length);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { dependencyLinks } from 'jest/packages/mock_data';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue';
|
||||
import { dependencyLinks } from '../../mock_data';
|
||||
|
||||
describe('DependencyRow', () => {
|
||||
let wrapper;
|
||||
|
||||
const { withoutFramework, withoutVersion, fullLink } = dependencyLinks;
|
||||
const [fullDependencyLink] = dependencyLinks();
|
||||
const { dependency, metadata } = fullDependencyLink;
|
||||
|
||||
function createComponent({ dependencyLink = fullLink } = {}) {
|
||||
wrapper = shallowMount(DependencyRow, {
|
||||
function createComponent(dependencyLink = fullDependencyLink) {
|
||||
wrapper = shallowMountExtended(DependencyRow, {
|
||||
propsData: {
|
||||
dependency: dependencyLink,
|
||||
dependencyLink,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const dependencyVersion = () => wrapper.find('[data-testid="version-pattern"]');
|
||||
const dependencyFramework = () => wrapper.find('[data-testid="target-framework"]');
|
||||
const dependencyVersion = () => wrapper.findByTestId('version-pattern');
|
||||
const dependencyFramework = () => wrapper.findByTestId('target-framework');
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -32,7 +33,10 @@ describe('DependencyRow', () => {
|
|||
|
||||
describe('version', () => {
|
||||
it('does not render any version information when not supplied', () => {
|
||||
createComponent({ dependencyLink: withoutVersion });
|
||||
createComponent({
|
||||
...fullDependencyLink,
|
||||
dependency: { ...dependency, versionPattern: undefined },
|
||||
});
|
||||
|
||||
expect(dependencyVersion().exists()).toBe(false);
|
||||
});
|
||||
|
@ -41,13 +45,16 @@ describe('DependencyRow', () => {
|
|||
createComponent();
|
||||
|
||||
expect(dependencyVersion().exists()).toBe(true);
|
||||
expect(dependencyVersion().text()).toBe(fullLink.version_pattern);
|
||||
expect(dependencyVersion().text()).toBe(dependency.versionPattern);
|
||||
});
|
||||
});
|
||||
|
||||
describe('target framework', () => {
|
||||
it('does not render any framework information when not supplied', () => {
|
||||
createComponent({ dependencyLink: withoutFramework });
|
||||
createComponent({
|
||||
...fullDependencyLink,
|
||||
metadata: { ...metadata, targetFramework: undefined },
|
||||
});
|
||||
|
||||
expect(dependencyFramework().exists()).toBe(false);
|
||||
});
|
||||
|
@ -56,7 +63,7 @@ describe('DependencyRow', () => {
|
|||
createComponent();
|
||||
|
||||
expect(dependencyFramework().exists()).toBe(true);
|
||||
expect(dependencyFramework().text()).toBe(`(${fullLink.target_framework})`);
|
||||
expect(dependencyFramework().text()).toBe(`(${metadata.targetFramework})`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -51,6 +51,41 @@ export const packageFiles = () => [
|
|||
},
|
||||
];
|
||||
|
||||
export const dependencyLinks = () => [
|
||||
{
|
||||
dependencyType: 'DEPENDENCIES',
|
||||
id: 'gid://gitlab/Packages::DependencyLink/77',
|
||||
__typename: 'PackageDependencyLink',
|
||||
dependency: {
|
||||
id: 'gid://gitlab/Packages::Dependency/3',
|
||||
name: 'Ninject.Extensions.Factory',
|
||||
versionPattern: '3.3.2',
|
||||
__typename: 'PackageDependency',
|
||||
},
|
||||
metadata: {
|
||||
id: 'gid://gitlab/Packages::Nuget::DependencyLinkMetadatum/77',
|
||||
targetFramework: '.NETCoreApp3.1',
|
||||
__typename: 'NugetDependencyLinkMetadata',
|
||||
},
|
||||
},
|
||||
{
|
||||
dependencyType: 'DEPENDENCIES',
|
||||
id: 'gid://gitlab/Packages::DependencyLink/78',
|
||||
__typename: 'PackageDependencyLink',
|
||||
dependency: {
|
||||
id: 'gid://gitlab/Packages::Dependency/4',
|
||||
name: 'Ninject.Extensions.Factory',
|
||||
versionPattern: '3.3.2',
|
||||
__typename: 'PackageDependency',
|
||||
},
|
||||
metadata: {
|
||||
id: 'gid://gitlab/Packages::Nuget::DependencyLinkMetadatum/78',
|
||||
targetFramework: '.NETCoreApp3.1',
|
||||
__typename: 'NugetDependencyLinkMetadata',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const packageVersions = () => [
|
||||
{
|
||||
createdAt: '2021-08-10T09:33:54Z',
|
||||
|
@ -145,6 +180,9 @@ export const packageDetailsQuery = (extendPackage) => ({
|
|||
nodes: packageVersions(),
|
||||
__typename: 'PackageConnection',
|
||||
},
|
||||
dependencyLinks: {
|
||||
nodes: dependencyLinks(),
|
||||
},
|
||||
__typename: 'PackageDetailsType',
|
||||
...extendPackage,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import { GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import InitCommandModal from '~/terraform/components/init_command_modal.vue';
|
||||
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
|
||||
|
||||
const accessTokensPath = '/path/to/access-tokens-page';
|
||||
const terraformApiUrl = 'https://gitlab.com/api/v4/projects/1';
|
||||
const username = 'username';
|
||||
const modalId = 'fake-modal-id';
|
||||
const stateName = 'production';
|
||||
const modalInfoCopyStr = `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
|
||||
terraform init \\
|
||||
-backend-config="address=${terraformApiUrl}/${stateName}" \\
|
||||
-backend-config="lock_address=${terraformApiUrl}/${stateName}/lock" \\
|
||||
-backend-config="unlock_address=${terraformApiUrl}/${stateName}/lock" \\
|
||||
-backend-config="username=${username}" \\
|
||||
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
|
||||
-backend-config="lock_method=POST" \\
|
||||
-backend-config="unlock_method=DELETE" \\
|
||||
-backend-config="retry_wait_min=5"
|
||||
`;
|
||||
|
||||
describe('InitCommandModal', () => {
|
||||
let wrapper;
|
||||
|
||||
const propsData = {
|
||||
modalId,
|
||||
stateName,
|
||||
};
|
||||
const provideData = {
|
||||
accessTokensPath,
|
||||
terraformApiUrl,
|
||||
username,
|
||||
};
|
||||
|
||||
const findExplanatoryText = () => wrapper.findByTestId('init-command-explanatory-text');
|
||||
const findLink = () => wrapper.findComponent(GlLink);
|
||||
const findInitCommand = () => wrapper.findByTestId('terraform-init-command');
|
||||
const findCopyButton = () => wrapper.findComponent(ModalCopyButton);
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMountExtended(InitCommandModal, {
|
||||
propsData,
|
||||
provide: provideData,
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('on rendering', () => {
|
||||
it('renders the explanatory text', () => {
|
||||
expect(findExplanatoryText().text()).toContain('personal access token');
|
||||
});
|
||||
|
||||
it('renders the personal access token link', () => {
|
||||
expect(findLink().attributes('href')).toBe(accessTokensPath);
|
||||
});
|
||||
|
||||
it('renders the init command with the username and state name prepopulated', () => {
|
||||
expect(findInitCommand().text()).toContain(username);
|
||||
expect(findInitCommand().text()).toContain(stateName);
|
||||
});
|
||||
|
||||
it('renders the copyToClipboard button', () => {
|
||||
expect(findCopyButton().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when copy button is clicked', () => {
|
||||
it('copies init command to clipboard', () => {
|
||||
expect(findCopyButton().props('text')).toBe(modalInfoCopyStr);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -3,6 +3,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
|
|||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import InitCommandModal from '~/terraform/components/init_command_modal.vue';
|
||||
import StateActions from '~/terraform/components/states_table_actions.vue';
|
||||
import lockStateMutation from '~/terraform/graphql/mutations/lock_state.mutation.graphql';
|
||||
import removeStateMutation from '~/terraform/graphql/mutations/remove_state.mutation.graphql';
|
||||
|
@ -73,12 +74,14 @@ describe('StatesTableActions', () => {
|
|||
return wrapper.vm.$nextTick();
|
||||
};
|
||||
|
||||
const findActionsDropdown = () => wrapper.find(GlDropdown);
|
||||
const findActionsDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findCopyBtn = () => wrapper.find('[data-testid="terraform-state-copy-init-command"]');
|
||||
const findCopyModal = () => wrapper.findComponent(InitCommandModal);
|
||||
const findLockBtn = () => wrapper.find('[data-testid="terraform-state-lock"]');
|
||||
const findUnlockBtn = () => wrapper.find('[data-testid="terraform-state-unlock"]');
|
||||
const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
|
||||
const findRemoveBtn = () => wrapper.find('[data-testid="terraform-state-remove"]');
|
||||
const findRemoveModal = () => wrapper.find(GlModal);
|
||||
const findRemoveModal = () => wrapper.findComponent(GlModal);
|
||||
|
||||
beforeEach(() => {
|
||||
return createComponent();
|
||||
|
@ -125,6 +128,25 @@ describe('StatesTableActions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('copy command button', () => {
|
||||
it('displays a copy init command button', () => {
|
||||
expect(findCopyBtn().text()).toBe('Copy Terraform init command');
|
||||
});
|
||||
|
||||
describe('when clicking the copy init command button', () => {
|
||||
beforeEach(() => {
|
||||
findCopyBtn().vm.$emit('click');
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('opens the modal', async () => {
|
||||
expect(findCopyModal().exists()).toBe(true);
|
||||
expect(findCopyModal().isVisible()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('download button', () => {
|
||||
it('displays a download button', () => {
|
||||
expect(findDownloadBtn().text()).toBe('Download JSON');
|
||||
|
|
|
@ -22,6 +22,18 @@ RSpec.describe Projects::TerraformHelper do
|
|||
expect(subject[:project_path]).to eq(project.full_path)
|
||||
end
|
||||
|
||||
it 'includes access token path' do
|
||||
expect(subject[:access_tokens_path]).to eq(profile_personal_access_tokens_path)
|
||||
end
|
||||
|
||||
it 'includes username' do
|
||||
expect(subject[:username]).to eq(current_user.username)
|
||||
end
|
||||
|
||||
it 'includes terraform state api url' do
|
||||
expect(subject[:terraform_api_url]).to eq("#{Settings.gitlab.url}/api/v4/projects/#{project.id}/terraform/state")
|
||||
end
|
||||
|
||||
it 'indicates the user is a terraform admin' do
|
||||
expect(subject[:terraform_admin]).to eq(true)
|
||||
end
|
||||
|
|
|
@ -160,6 +160,36 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
|
|||
|
||||
it_behaves_like 'a listing of new commits'
|
||||
end
|
||||
|
||||
context 'with criss-cross merges' do
|
||||
let(:new_commits) do
|
||||
[
|
||||
create_commit(newrev, %w[a1 b1]),
|
||||
create_commit('a1', %w[a2 b2]),
|
||||
create_commit('a2', %w[a3 b3]),
|
||||
create_commit('a3', %w[c]),
|
||||
create_commit('b1', %w[b2 a2]),
|
||||
create_commit('b2', %w[b3 a3]),
|
||||
create_commit('b3', %w[c]),
|
||||
create_commit('c', [])
|
||||
]
|
||||
end
|
||||
|
||||
let(:expected_commits) do
|
||||
[
|
||||
create_commit(newrev, %w[a1 b1]),
|
||||
create_commit('a1', %w[a2 b2]),
|
||||
create_commit('b1', %w[b2 a2]),
|
||||
create_commit('a2', %w[a3 b3]),
|
||||
create_commit('b2', %w[b3 a3]),
|
||||
create_commit('a3', %w[c]),
|
||||
create_commit('b3', %w[c]),
|
||||
create_commit('c', [])
|
||||
]
|
||||
end
|
||||
|
||||
it_behaves_like 'a listing of new commits'
|
||||
end
|
||||
end
|
||||
|
||||
def create_commit(id, parent_ids)
|
||||
|
|
|
@ -111,4 +111,35 @@ RSpec.describe Gitlab::Instrumentation::RedisInterceptor, :clean_gitlab_redis_sh
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a command takes longer than DURATION_ERROR_THRESHOLD' do
|
||||
let(:threshold) { 0.5 }
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::DURATION_ERROR_THRESHOLD", threshold)
|
||||
end
|
||||
|
||||
context 'when report_on_long_redis_durations is disabled' do
|
||||
it 'does nothing' do
|
||||
stub_feature_flags(report_on_long_redis_durations: false)
|
||||
|
||||
expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
|
||||
|
||||
Gitlab::Redis::SharedState.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when report_on_long_redis_durations is enabled' do
|
||||
it 'tracks an exception and continues' do
|
||||
expect(Gitlab::ErrorTracking)
|
||||
.to receive(:track_exception)
|
||||
.with(an_instance_of(described_class::MysteryRedisDurationError),
|
||||
command: 'mget',
|
||||
duration: be > threshold,
|
||||
timestamp: a_string_matching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{5}/))
|
||||
|
||||
Gitlab::Redis::SharedState.with { |r| r.mget('foo', 'foo') { sleep threshold + 0.1 } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3743,9 +3743,23 @@ RSpec.describe Ci::Build do
|
|||
context 'when artifacts of depended job has been expired' do
|
||||
let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
|
||||
|
||||
context 'when pipeline is not locked' do
|
||||
before do
|
||||
build.pipeline.unlocked!
|
||||
end
|
||||
|
||||
it { expect(job).not_to have_valid_build_dependencies }
|
||||
end
|
||||
|
||||
context 'when pipeline is locked' do
|
||||
before do
|
||||
build.pipeline.artifacts_locked!
|
||||
end
|
||||
|
||||
it { expect(job).to have_valid_build_dependencies }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when artifacts of depended job has been erased' do
|
||||
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
|
||||
|
||||
|
@ -4763,11 +4777,27 @@ RSpec.describe Ci::Build do
|
|||
let!(:pre_stage_job_invalid) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test2', stage_idx: 1) }
|
||||
let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 2, options: { dependencies: %w(test1 test2) }) }
|
||||
|
||||
it 'returns invalid dependencies' do
|
||||
context 'when pipeline is locked' do
|
||||
before do
|
||||
build.pipeline.unlocked!
|
||||
end
|
||||
|
||||
it 'returns invalid dependencies when expired' do
|
||||
expect(job.invalid_dependencies).to eq([pre_stage_job_invalid])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pipeline is not locked' do
|
||||
before do
|
||||
build.pipeline.artifacts_locked!
|
||||
end
|
||||
|
||||
it 'returns no invalid dependencies when expired' do
|
||||
expect(job.invalid_dependencies).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#execute_hooks' do
|
||||
before do
|
||||
build.clear_memoization(:build_data)
|
||||
|
|
|
@ -6,7 +6,6 @@ RSpec.describe Project, factory_default: :keep do
|
|||
include ProjectForksHelper
|
||||
include GitHelpers
|
||||
include ExternalAuthorizationServiceHelpers
|
||||
include ReloadHelpers
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let_it_be(:namespace) { create_default(:namespace).freeze }
|
||||
|
@ -3022,72 +3021,31 @@ RSpec.describe Project, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
shared_context 'project with ancestors' do
|
||||
describe '#ancestors_upto' do
|
||||
let_it_be(:parent) { create(:group) }
|
||||
let_it_be(:child) { create(:group, parent: parent) }
|
||||
let_it_be(:child2) { create(:group, parent: child) }
|
||||
let_it_be(:project) { create(:project, namespace: child2) }
|
||||
end
|
||||
|
||||
shared_examples '#ancestors' do
|
||||
before do
|
||||
reload_models(parent, child, child2)
|
||||
it 'returns all ancestors when no namespace is given' do
|
||||
expect(project.ancestors_upto).to contain_exactly(child2, child, parent)
|
||||
end
|
||||
|
||||
it 'returns all ancestors' do
|
||||
expect(project.ancestors).to contain_exactly(child2, child, parent)
|
||||
end
|
||||
|
||||
describe 'with hierarchy_order' do
|
||||
it 'returns ancestors ordered by descending hierarchy' do
|
||||
expect(project.ancestors(hierarchy_order: :desc).to_a).to eq([parent, child, child2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ancestors' do
|
||||
include_context 'project with ancestors'
|
||||
|
||||
include_examples '#ancestors'
|
||||
end
|
||||
|
||||
describe '#ancestors_upto' do
|
||||
include_context 'project with ancestors'
|
||||
|
||||
include_examples '#ancestors'
|
||||
|
||||
it 'includes ancestors upto but excluding the given ancestor' do
|
||||
expect(project.ancestors_upto(parent)).to contain_exactly(child2, child)
|
||||
end
|
||||
|
||||
describe 'with hierarchy_order' do
|
||||
it 'returns ancestors ordered by descending hierarchy' do
|
||||
expect(project.ancestors_upto(hierarchy_order: :desc)).to eq([parent, child, child2])
|
||||
end
|
||||
|
||||
it 'can be used with upto option' do
|
||||
expect(project.ancestors_upto(parent, hierarchy_order: :desc)).to eq([child, child2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ancestors' do
|
||||
let_it_be(:parent) { create(:group) }
|
||||
let_it_be(:child) { create(:group, parent: parent) }
|
||||
let_it_be(:child2) { create(:group, parent: child) }
|
||||
let_it_be(:project) { create(:project, namespace: child2) }
|
||||
|
||||
before do
|
||||
reload_models(parent, child, child2)
|
||||
end
|
||||
|
||||
it 'returns all ancestors' do
|
||||
expect(project.ancestors).to contain_exactly(child2, child, parent)
|
||||
end
|
||||
|
||||
describe 'with hierarchy_order' do
|
||||
it 'returns ancestors ordered by descending hierarchy' do
|
||||
expect(project.ancestors(hierarchy_order: :desc).to_a).to eq([parent, child, child2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#root_ancestor' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
|
|
|
@ -133,6 +133,7 @@ RSpec.describe BuildDetailsEntity do
|
|||
let(:message) { subject[:callout_message] }
|
||||
|
||||
before do
|
||||
build.pipeline.unlocked!
|
||||
build.drop!(:missing_dependency_failure)
|
||||
end
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ require 'spec_helper'
|
|||
module Ci
|
||||
RSpec.describe RegisterJobService do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project, reload: true) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
let_it_be_with_reload(:project) { create(:project, group: group, shared_runners_enabled: false, group_runners_enabled: false) }
|
||||
let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
|
||||
let!(:shared_runner) { create(:ci_runner, :instance) }
|
||||
let!(:specific_runner) { create(:ci_runner, :project, projects: [project]) }
|
||||
|
@ -467,14 +467,28 @@ module Ci
|
|||
context 'when depended job has not been completed yet' do
|
||||
let!(:pre_stage_job) { create(:ci_build, :pending, :queued, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
|
||||
|
||||
it { expect(subject).to eq(pending_job) }
|
||||
it { is_expected.to eq(pending_job) }
|
||||
end
|
||||
|
||||
context 'when artifacts of depended job has been expired' do
|
||||
let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
|
||||
|
||||
context 'when the pipeline is locked' do
|
||||
before do
|
||||
pipeline.artifacts_locked!
|
||||
end
|
||||
|
||||
it { is_expected.to eq(pending_job) }
|
||||
end
|
||||
|
||||
context 'when the pipeline is unlocked' do
|
||||
before do
|
||||
pipeline.unlocked!
|
||||
end
|
||||
|
||||
it_behaves_like 'not pick'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when artifacts of depended job has been erased' do
|
||||
let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
|
||||
|
@ -490,9 +504,13 @@ module Ci
|
|||
let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Ci::Build).to receive(:drop!)
|
||||
pipeline.unlocked!
|
||||
|
||||
allow_next_instance_of(Ci::Build) do |build|
|
||||
expect(build).to receive(:drop!)
|
||||
.and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not drop nor pick' do
|
||||
expect(subject).to be_nil
|
||||
|
|
|
@ -34,11 +34,9 @@ RSpec.shared_examples 'package details link' do |property|
|
|||
|
||||
expect(page).to have_css('.packages-app h1[data-testid="title"]', text: package.name)
|
||||
|
||||
page.within(%Q([name="#{package.name}"])) do
|
||||
expect(page).to have_content('Installation')
|
||||
expect(page).to have_content('Registry setup')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'when there are no packages' do
|
||||
|
|
Loading…
Reference in a new issue