Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-25 15:08:37 +00:00
parent 333f76ab66
commit 65f5f75e3a
51 changed files with 1072 additions and 124 deletions

View File

@ -13,11 +13,35 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql';
import { __, sprintf } from '~/locale';
import Api from '~/api';
let eTagPoll;
export const updateConfidentialityOnIssue = ({ commit, getters }, { confidential, fullPath }) => {
const { iid } = getters.getNoteableData;
return utils.gqClient
.mutate({
mutation: updateIssueConfidentialMutation,
variables: {
input: {
projectPath: fullPath,
iid: String(iid),
confidential,
},
},
})
.then(({ data }) => {
const {
issueSetConfidential: { issue },
} = data;
commit(types.SET_ISSUE_CONFIDENTIAL, issue.confidential);
});
};
export const expandDiscussion = ({ commit, dispatch }, data) => {
if (data.discussionId) {
dispatch('diffs/renderFileForDiscussionId', data.discussionId, { root: true });
@ -32,6 +56,8 @@ export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, d
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setConfidentiality = ({ commit }, data) => commit(types.SET_ISSUE_CONFIDENTIAL, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);

View File

@ -39,6 +39,7 @@ export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING';
export const SET_ISSUE_CONFIDENTIAL = 'SET_ISSUE_CONFIDENTIAL';
// Description version
export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION';

View File

@ -95,6 +95,10 @@ export default {
Object.assign(state, { noteableData: data });
},
[types.SET_ISSUE_CONFIDENTIAL](state, data) {
state.noteableData.confidential = data;
},
[types.SET_USER_DATA](state, data) {
Object.assign(state, { userData: data });
},

View File

@ -1,6 +1,7 @@
import AjaxCache from '~/lib/utils/ajax_cache';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
import { sprintf, __ } from '~/locale';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
// factory function because global flag makes RegExp stateful
const createQuickActionsRegex = () => /^\/\w+.*$/gm;
@ -34,3 +35,10 @@ export const stripQuickActions = note => note.replace(createQuickActionsRegex(),
export const prepareDiffLines = diffLines =>
diffLines.map(line => ({ ...trimFirstCharOfLineContent(line) }));
export const gqClient = createGqClient(
{},
{
fetchPolicy: fetchPolicies.NO_CACHE,
},
);

View File

@ -33,7 +33,7 @@ export default {
errorTexts: {
[LOAD_FAILURE]: __('We are currently unable to fetch data for this graph.'),
[PARSE_FAILURE]: __('There was an error parsing the data for this graph.'),
[UNSUPPORTED_DATA]: __('A DAG must have two dependent jobs to be visualized on this tab.'),
[UNSUPPORTED_DATA]: __('DAG visualization requires at least 3 dependent jobs.'),
[DEFAULT]: __('An unknown error occurred while loading this graph.'),
},
computed: {

View File

@ -1,5 +1,5 @@
<script>
import { mapState } from 'vuex';
import { mapState, mapActions } from 'vuex';
import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
@ -18,6 +18,10 @@ export default {
},
mixins: [recaptchaModalImplementor],
props: {
fullPath: {
required: true,
type: String,
},
isEditable: {
required: true,
type: Boolean,
@ -42,16 +46,24 @@ export default {
},
},
created() {
eventHub.$on('updateConfidentialAttribute', this.updateConfidentialAttribute);
eventHub.$on('closeConfidentialityForm', this.toggleForm);
},
beforeDestroy() {
eventHub.$off('updateConfidentialAttribute', this.updateConfidentialAttribute);
eventHub.$off('closeConfidentialityForm', this.toggleForm);
},
methods: {
...mapActions(['setConfidentiality']),
toggleForm() {
this.edit = !this.edit;
},
updateConfidentialAttribute(confidential) {
closeForm() {
this.edit = false;
},
updateConfidentialAttribute() {
// TODO: rm when FF is defaulted to on.
const confidential = !this.confidential;
this.service
.update('issue', { confidential })
.then(({ data }) => this.checkForSpam(data))
@ -97,12 +109,8 @@ export default {
>
</div>
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="edit"
:is-confidential="confidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
<div v-if="!confidential" class="no-value sidebar-item-value">
<edit-form v-if="edit" :is-confidential="confidential" :full-path="fullPath" />
<div v-if="!confidential" class="no-value sidebar-item-value" data-testid="not-confidential">
<icon :size="16" name="eye" aria-hidden="true" class="sidebar-item-icon inline" />
{{ __('Not confidential') }}
</div>

View File

@ -11,9 +11,9 @@ export default {
required: true,
type: Boolean,
},
updateConfidentialAttribute: {
fullPath: {
required: true,
type: Function,
type: String,
},
},
computed: {
@ -37,10 +37,7 @@ export default {
<div>
<p v-if="!isConfidential" v-html="confidentialityOnWarning"></p>
<p v-else v-html="confidentialityOffWarning"></p>
<edit-form-buttons
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
<edit-form-buttons :full-path="fullPath" />
</div>
</div>
</div>

View File

@ -1,35 +1,60 @@
<script>
import $ from 'jquery';
import eventHub from '../../event_hub';
import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Flash from '~/flash';
import eventHub from '../../event_hub';
export default {
components: {
GlLoadingIcon,
},
mixins: [glFeatureFlagsMixin()],
props: {
isConfidential: {
fullPath: {
required: true,
type: Boolean,
},
updateConfidentialAttribute: {
required: true,
type: Function,
type: String,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
...mapState({ confidential: ({ noteableData }) => noteableData.confidential }),
toggleButtonText() {
return this.isConfidential ? __('Turn Off') : __('Turn On');
},
updateConfidentialBool() {
return !this.isConfidential;
if (this.isLoading) {
return __('Applying');
}
return this.confidential ? __('Turn Off') : __('Turn On');
},
},
methods: {
...mapActions(['updateConfidentialityOnIssue']),
closeForm() {
eventHub.$emit('closeConfidentialityForm');
$(this.$el).trigger('hidden.gl.dropdown');
},
submitForm() {
this.closeForm();
this.updateConfidentialAttribute(this.updateConfidentialBool);
this.isLoading = true;
const confidential = !this.confidential;
if (this.glFeatures.confidentialApolloSidebar) {
this.updateConfidentialityOnIssue({ confidential, fullPath: this.fullPath })
.catch(() => {
Flash(__('Something went wrong trying to change the confidentiality of this issue'));
})
.finally(() => {
this.closeForm();
this.isLoading = false;
});
} else {
eventHub.$emit('updateConfidentialAttribute');
}
},
},
};
@ -44,8 +69,10 @@ export default {
type="button"
class="btn btn-close"
data-testid="confidential-toggle"
:disabled="isLoading"
@click.prevent="submitForm"
>
<gl-loading-icon v-if="isLoading" inline />
{{ toggleButtonText }}
</button>
</div>

View File

@ -0,0 +1,7 @@
mutation updateIssueConfidential($input: IssueSetConfidentialInput!) {
issueSetConfidential(input: $input) {
issue {
confidential
}
}
}

View File

@ -52,20 +52,30 @@ function mountAssigneesComponent(mediator) {
function mountConfidentialComponent(mediator) {
const el = document.getElementById('js-confidential-entry-point');
const { fullPath, iid } = getSidebarOptions();
if (!el) return;
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
const ConfidentialComp = Vue.extend(ConfidentialIssueSidebar);
new ConfidentialComp({
// eslint-disable-next-line no-new
new Vue({
el,
store,
propsData: {
isEditable: initialData.is_editable,
service: mediator.service,
components: {
ConfidentialIssueSidebar,
},
}).$mount(el);
render: createElement =>
createElement('confidential-issue-sidebar', {
props: {
iid: String(iid),
fullPath,
isEditable: initialData.is_editable,
service: mediator.service,
},
}),
});
}
function mountLockComponent(mediator) {

View File

@ -415,7 +415,6 @@ img.emoji {
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-right-48 { margin-right: 48px; }
.prepend-right-32 { margin-right: 32px; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; }

View File

@ -51,6 +51,8 @@ class Projects::IssuesController < Projects::ApplicationController
before_action only: :show do
push_frontend_feature_flag(:real_time_issue_sidebar, @project)
push_frontend_feature_flag(:confidential_notes, @project)
push_frontend_feature_flag(:confidential_apollo_sidebar, @project)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]

View File

@ -320,7 +320,9 @@ module Ci
# ref - The ref to scope the data to (e.g. "master"). If the ref is not
# given we simply get the latest pipelines for the commits, regardless
# of what refs the pipelines belong to.
def self.latest_pipeline_per_commit(commits, ref = nil)
# project_key - Support `commits` from different projects, returns results
# keyed by `hash[project_id][commit_id]`
def self.latest_pipeline_per_commit(commits, ref = nil, project_key: false)
p1 = arel_table
p2 = arel_table.alias
@ -341,7 +343,13 @@ module Ci
relation = relation.where(ref: ref) if ref
relation.each_with_object({}) do |pipeline, hash|
hash[pipeline.sha] = pipeline
commits = if project_key
hash[pipeline.project_id] ||= {}
else
hash
end
commits[pipeline.sha] = pipeline
end
end
@ -349,6 +357,10 @@ module Ci
success.group(:project_id).select('max(id) as id')
end
def self.last_finished_for_ref_id(ci_ref_id)
where(ci_ref_id: ci_ref_id).ci_sources.finished.order(id: :desc).select(:id).take
end
def self.truncate_sha(sha)
sha[0...8]
end

View File

@ -45,7 +45,8 @@ module Ci
webide_source: 3,
remote_source: 4,
external_project_source: 5,
bridge_source: 6
bridge_source: 6,
parameter_source: 7
}
end

View File

@ -43,7 +43,7 @@ module Ci
end
def last_finished_pipeline_id
Ci::Pipeline.where(ci_ref_id: self.id).finished.order(id: :desc).select(:id).take&.id
Ci::Pipeline.last_finished_for_ref_id(self.id)&.id
end
def update_status_by!(pipeline)

View File

@ -1,16 +1,13 @@
# frozen_string_literal: true
# A collection of Commit instances for a specific container and Git reference.
# A collection of Commit instances for a specific Git reference.
class CommitCollection
include Enumerable
include Gitlab::Utils::StrongMemoize
attr_reader :container, :ref, :commits
delegate :repository, to: :container, allow_nil: true
delegate :project, to: :repository, allow_nil: true
# container - The object the commits belong to.
# container - The object the commits belong to (each commit project will be used if not provided).
# commits - The Commit instances to store.
# ref - The name of the ref (e.g. "master").
def initialize(container, commits, ref = nil)
@ -42,12 +39,13 @@ class CommitCollection
# Setting the pipeline for each commit ahead of time removes the need for running
# a query for every commit we're displaying.
def with_latest_pipeline(ref = nil)
return self unless project
pipelines = project.ci_pipelines.latest_pipeline_per_commit(map(&:id), ref)
# since commit ids are not unique across all projects, use project_key = true to get commits by project
pipelines = ::Ci::Pipeline.ci_sources.latest_pipeline_per_commit(map(&:id), ref, project_key: true)
# set the pipeline for each commit by project_id and commit for the latest pipeline for ref
each do |commit|
commit.set_latest_pipeline_for_ref(ref, pipelines[commit.id])
project_id = container&.id || commit.project_id
commit.set_latest_pipeline_for_ref(ref, pipelines.dig(project_id, commit.id))
end
self
@ -64,16 +62,19 @@ class CommitCollection
# Batch load any commits that are not backed by full gitaly data, and
# replace them in the collection.
def enrich!
return self if fully_enriched?
# Batch load full Commits from the repository
# and map to a Hash of id => Commit
# A container is needed in order to fetch data from gitaly. Containers
# can be absent from commits in certain rare situations (like when
# viewing a MR of a deleted fork). In these cases, assume that the
# enriched data is not needed.
return self if container.blank? || fully_enriched?
# Batch load full Commits from the repository
# and map to a Hash of id => Commit
replacements = Hash[unenriched.map do |c|
[c.id, Commit.lazy(container, c.id)]
commits_to_enrich = unenriched.select { |c| container.present? || c.container.present? }
replacements = Hash[commits_to_enrich.map do |c|
commit_container = container || c.container
[c.id, Commit.lazy(commit_container, c.id)]
end.compact]
# Replace the commits, keeping the same order

View File

@ -520,6 +520,10 @@ class Project < ApplicationRecord
group: :ip_restrictions, namespace: [:route, :owner])
}
scope :with_api_commit_entity_associations, -> {
preload(:project_feature, :route, namespace: [:route, :owner])
}
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
chronic_duration_attr :build_timeout_human_readable, :build_timeout,
@ -1699,7 +1703,7 @@ class Project < ApplicationRecord
def pages_url
url = pages_group_url
url_path = full_path.partition('/').last
url_path = full_path.partition('/').last.downcase
# If the project path is the same as host, we serve it as group page
return url if url == "#{Settings.pages.protocol}://#{url_path}"

View File

@ -23,6 +23,24 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::Activity,
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
# Create a new pipeline in the specified project.
#
# @param [Symbol] source What event (Ci::Pipeline.sources) triggers the pipeline
# creation.
# @param [Boolean] ignore_skip_ci Whether skipping a pipeline creation when `[skip ci]` comment
# is present in the commit body
# @param [Boolean] save_on_errors Whether persisting an invalid pipeline when it encounters an
# error during creation (e.g. invalid yaml)
# @param [Ci::TriggerRequest] trigger_request The pipeline trigger triggers the pipeline creation.
# @param [Ci::PipelineSchedule] schedule The pipeline schedule triggers the pipeline creation.
# @param [MergeRequest] merge_request The merge request triggers the pipeline creation.
# @param [ExternalPullRequest] external_pull_request The external pull request triggers the pipeline creation.
# @param [Ci::Bridge] bridge The bridge job that triggers the downstream pipeline creation.
# @param [String] content The content of .gitlab-ci.yml to override the default config
# contents (e.g. .gitlab-ci.yml in repostiry). Mainly used for
# generating a dangling pipeline.
#
# @return [Ci::Pipeline] The created Ci::Pipeline object.
# rubocop: disable Metrics/ParameterLists
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block)
@pipeline = Ci::Pipeline.new
@ -122,13 +140,8 @@ module Ci
end
end
def extra_options(options = {})
# In Ruby 2.4, even when options is empty, f(**options) doesn't work when f
# doesn't have any parameters. We reproduce the Ruby 2.5 behavior by
# checking explicitly that no arguments are given.
raise ArgumentError if options.any?
{} # overridden in EE
def extra_options(content: nil)
{ content: content }
end
end
end

View File

@ -76,7 +76,7 @@ module Projects
end
def parsed_payload
Gitlab::Alerting::NotificationPayloadParser.call(params.to_h)
Gitlab::Alerting::NotificationPayloadParser.call(params.to_h, project)
end
def valid_token?(token)

View File

@ -0,0 +1,5 @@
---
title: Edit copy of DAG unsupported data alert
merge_request: 35170
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix pages_url for projects with mixed case path
merge_request: 35300
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Ability to use an arbitrary YAML blob to create CI pipelines
merge_request: 34706
author:
type: added

View File

@ -1346,6 +1346,51 @@ enum CommitEncoding {
TEXT
}
"""
Represents a ComplianceFramework associated with a Project
"""
type ComplianceFramework {
"""
Name of the compliance framework
"""
name: ProjectSettingEnum!
}
"""
The connection type for ComplianceFramework.
"""
type ComplianceFrameworkConnection {
"""
A list of edges.
"""
edges: [ComplianceFrameworkEdge]
"""
A list of nodes.
"""
nodes: [ComplianceFramework]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type ComplianceFrameworkEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: ComplianceFramework
}
"""
A tag expiration policy designed to keep only the images that matter most
"""
@ -8750,6 +8795,31 @@ type Project {
last: Int
): BoardConnection
"""
Compliance frameworks associated with the project
"""
complianceFrameworks(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): ComplianceFrameworkConnection
"""
The container expiration policy of the project
"""
@ -10026,6 +10096,17 @@ type ProjectPermissions {
uploadFile: Boolean!
}
"""
Names of compliance frameworks that can be assigned to a Project
"""
enum ProjectSettingEnum {
gdpr
hipaa
pci_dss
soc_2
sox
}
type ProjectStatistics {
"""
Build artifacts size of the project

View File

@ -3607,6 +3607,149 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ComplianceFramework",
"description": "Represents a ComplianceFramework associated with a Project",
"fields": [
{
"name": "name",
"description": "Name of the compliance framework",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "ProjectSettingEnum",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ComplianceFrameworkConnection",
"description": "The connection type for ComplianceFramework.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ComplianceFrameworkEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ComplianceFramework",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ComplianceFrameworkEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ComplianceFramework",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ContainerExpirationPolicy",
@ -26105,6 +26248,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "complianceFrameworks",
"description": "Compliance frameworks associated with the project",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ComplianceFrameworkConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "containerExpirationPolicy",
"description": "The container expiration policy of the project",
@ -29593,6 +29789,47 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "ProjectSettingEnum",
"description": "Names of compliance frameworks that can be assigned to a Project",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "gdpr",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "hipaa",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pci_dss",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "soc_2",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sox",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ProjectStatistics",

View File

@ -238,6 +238,14 @@ Autogenerated return type of CommitCreate
| `commit` | Commit | The commit after mutation |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## ComplianceFramework
Represents a ComplianceFramework associated with a Project
| Name | Type | Description |
| --- | ---- | ---------- |
| `name` | ProjectSettingEnum! | Name of the compliance framework |
## ContainerExpirationPolicy
A tag expiration policy designed to keep only the images that matter most

View File

@ -225,6 +225,47 @@ work between two different workers, one with `urgency :high` code that
executes quickly, and the other with `urgency :low`, which has no
execution latency requirements (but also has lower scheduling targets).
### Changing a queue's urgency
On GitLab.com, we run Sidekiq in several
[shards](https://dashboards.gitlab.net/d/sidekiq-shard-detail/sidekiq-shard-detail),
each of which represents a particular type of workload.
When changing a queue's urgency, or adding a new queue, we need to take
into account the expected workload on the new shard. Note that, if we're
changing an existing queue, there is also an effect on the old shard,
but that will always be a reduction in work.
To do this, we want to calculate the expected increase in total execution time
and RPS (throughput) for the new shard. We can get these values from:
- The [Queue Detail
dashboard](https://dashboards.gitlab.net/d/sidekiq-queue-detail/sidekiq-queue-detail)
has values for the queue itself. For a new queue, we can look for
queues that have similar patterns or are scheduled in similar
circumstances.
- The [Shard Detail
dashboard](https://dashboards.gitlab.net/d/sidekiq-shard-detail/sidekiq-shard-detail)
has Total Execution Time and Throughput (RPS). The Shard Utilization
panel will show if there is currently any excess capacity for this
shard.
We can then calculate the RPS * average runtime (estimated for new jobs)
for the queue we're changing to see what the relative increase in RPS and
execution time we expect for the new shard:
```ruby
new_queue_consumption = queue_rps * queue_duration_avg
shard_consumption = shard_rps * shard_duration_avg
(new_queue_consumption / shard_consumption) * 100
```
If we expect an increase of **less than 5%**, then no further action is needed.
Otherwise, please ping `@gitlab-org/scalability` on the merge request and ask
for a review.
## Jobs with External Dependencies
Most background jobs in the GitLab application communicate with other GitLab

View File

@ -8,14 +8,15 @@ module API
expose :project_id
expose :last_pipeline do |commit, options|
pipeline = commit.last_pipeline if can_read_pipeline?
pipeline = commit.latest_pipeline if can_read_pipeline?
::API::Entities::PipelineBasic.represent(pipeline, options)
end
private
def can_read_pipeline?
Ability.allowed?(options[:current_user], :read_pipeline, object.last_pipeline)
Ability.allowed?(options[:current_user], :read_pipeline, object.latest_pipeline)
end
end
end

View File

@ -24,7 +24,8 @@ module API
merge_requests: :with_api_entity_associations,
projects: :with_api_entity_associations,
issues: :with_api_entity_associations,
milestones: :with_api_entity_associations
milestones: :with_api_entity_associations,
commits: :with_api_commit_entity_associations
}.freeze
def search(additional_params = {})
@ -38,6 +39,9 @@ module API
results = SearchService.new(current_user, search_params).search_objects(preload_method)
# preload commit data
results = CommitCollection.new(nil, results).with_latest_pipeline if params[:scope].to_sym == :commits
Gitlab::UsageDataCounters::SearchCounter.count(:all_searches)
paginate(results)

View File

@ -8,7 +8,7 @@ module Gitlab
}.freeze
def self.from_generic_alert(project:, payload:)
parsed_payload = Gitlab::Alerting::NotificationPayloadParser.call(payload).with_indifferent_access
parsed_payload = Gitlab::Alerting::NotificationPayloadParser.call(payload, project).with_indifferent_access
annotations = parsed_payload[:annotations]
{

View File

@ -10,11 +10,14 @@ module Gitlab
def generate(data)
return unless data.present?
if data.is_a?(Array)
data = flatten_array(data)
end
string = case data
when Array then flatten_array(data)
when Hash then flatten_hash(data)
else
data.to_s
end
Digest::SHA1.hexdigest(data.to_s)
Digest::SHA1.hexdigest(string)
end
private
@ -22,6 +25,11 @@ module Gitlab
def flatten_array(array)
array.flatten.map!(&:to_s).join
end
def flatten_hash(hash)
# Sort hash so SHA generated is the same
Gitlab::Utils::SafeInlineHash.merge_keys!(hash).sort.to_s
end
end
end
end

View File

@ -8,12 +8,13 @@ module Gitlab
DEFAULT_TITLE = 'New: Incident'
DEFAULT_SEVERITY = 'critical'
def initialize(payload)
def initialize(payload, project)
@payload = payload.to_h.with_indifferent_access
@project = project
end
def self.call(payload)
new(payload).call
def self.call(payload, project)
new(payload, project).call
end
def call
@ -25,7 +26,7 @@ module Gitlab
private
attr_reader :payload
attr_reader :payload, :project
def title
payload[:title].presence || DEFAULT_TITLE
@ -84,3 +85,5 @@ module Gitlab
end
end
end
Gitlab::Alerting::NotificationPayloadParser.prepend_if_ee('EE::Gitlab::Alerting::NotificationPayloadParser')

View File

@ -10,7 +10,7 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options,
:chat_data, :allow_mirror_update, :bridge,
:chat_data, :allow_mirror_update, :bridge, :content,
# These attributes are set by Chains during processing:
:config_content, :config_processor, :stage_seeds
) do

View File

@ -9,6 +9,7 @@ module Gitlab
include Chain::Helpers
SOURCES = [
Gitlab::Ci::Pipeline::Chain::Config::Content::Parameter,
Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
Gitlab::Ci::Pipeline::Chain::Config::Content::Repository,
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Chain
module Config
class Content
class Parameter < Source
def content
strong_memoize(:content) do
next unless command.content.present?
command.content
end
end
def source
:parameter_source
end
end
end
end
end
end
end
end

View File

@ -11,6 +11,8 @@ module Gitlab
DEFAULT_YAML_FILE = '.gitlab-ci.yml'
attr_reader :command
def initialize(pipeline, command)
@pipeline = pipeline
@command = command

View File

@ -962,9 +962,6 @@ msgstr ""
msgid "A .NET Core console application template, customizable for any .NET Core project"
msgstr ""
msgid "A DAG must have two dependent jobs to be visualized on this tab."
msgstr ""
msgid "A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features."
msgstr ""
@ -2685,6 +2682,9 @@ msgstr ""
msgid "Apply this approval rule to any branch or a specific protected branch."
msgstr ""
msgid "Applying"
msgstr ""
msgid "Applying a template will replace the existing issue description. Any changes you have made will be lost."
msgstr ""
@ -7105,6 +7105,9 @@ msgstr ""
msgid "DAG"
msgstr ""
msgid "DAG visualization requires at least 3 dependent jobs."
msgstr ""
msgid "DNS"
msgstr ""

View File

@ -40,8 +40,8 @@
"@babel/plugin-syntax-import-meta": "^7.10.1",
"@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.141.0",
"@gitlab/ui": "17.2.0",
"@gitlab/svgs": "1.143.0",
"@gitlab/ui": "17.6.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.10.2",

View File

@ -18,6 +18,8 @@ import {
batchSuggestionsInfoMock,
} from '../mock_data';
import axios from '~/lib/utils/axios_utils';
import * as utils from '~/notes/stores/utils';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql';
const TEST_ERROR_MESSAGE = 'Test error message';
jest.mock('~/flash');
@ -1142,6 +1144,14 @@ describe('Actions Notes Store', () => {
});
});
describe('setConfidentiality', () => {
it('calls the correct mutation with the correct args', () => {
testAction(actions.setConfidentiality, true, { noteableData: { confidential: false } }, [
{ type: mutationTypes.SET_ISSUE_CONFIDENTIAL, payload: true },
]);
});
});
describe('updateAssignees', () => {
it('update the assignees state', done => {
testAction(
@ -1154,4 +1164,49 @@ describe('Actions Notes Store', () => {
);
});
});
describe('updateConfidentialityOnIssue', () => {
state = { noteableData: { confidential: false } };
const iid = '1';
const projectPath = 'full/path';
const getters = { getNoteableData: { iid } };
const actionArgs = { fullPath: projectPath, confidential: true };
const confidential = true;
beforeEach(() => {
jest
.spyOn(utils.gqClient, 'mutate')
.mockResolvedValue({ data: { issueSetConfidential: { issue: { confidential } } } });
});
it('calls gqClient mutation one time', () => {
actions.updateConfidentialityOnIssue({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1);
});
it('calls gqClient mutation with the correct values', () => {
actions.updateConfidentialityOnIssue({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledWith({
mutation: updateIssueConfidentialMutation,
variables: { input: { iid, projectPath, confidential } },
});
});
describe('on success of mutation', () => {
it('calls commit with the correct values', () => {
const commitSpy = jest.fn();
return actions
.updateConfidentialityOnIssue({ commit: commitSpy, state, getters }, actionArgs)
.then(() => {
expect(commitSpy).toHaveBeenCalledWith(
mutationTypes.SET_ISSUE_CONFIDENTIAL,
confidential,
);
});
});
});
});
});

View File

@ -806,6 +806,20 @@ describe('Notes Store mutations', () => {
});
});
describe('SET_ISSUE_CONFIDENTIAL', () => {
let state;
beforeEach(() => {
state = { noteableData: { confidential: false } };
});
it('sets sort order', () => {
mutations.SET_ISSUE_CONFIDENTIAL(state, true);
expect(state.noteableData.confidential).toBe(true);
});
});
describe('UPDATE_ASSIGNEES', () => {
it('should update assignees', () => {
const state = {

View File

@ -3,6 +3,7 @@
exports[`Confidential Issue Sidebar Block renders for confidential = false and isEditable = false 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
iid=""
>
<div
class="sidebar-collapsed-icon"
@ -35,6 +36,7 @@ exports[`Confidential Issue Sidebar Block renders for confidential = false and i
<div
class="no-value sidebar-item-value"
data-testid="not-confidential"
>
<icon-stub
aria-hidden="true"
@ -55,6 +57,7 @@ exports[`Confidential Issue Sidebar Block renders for confidential = false and i
exports[`Confidential Issue Sidebar Block renders for confidential = false and isEditable = true 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
iid=""
>
<div
class="sidebar-collapsed-icon"
@ -95,6 +98,7 @@ exports[`Confidential Issue Sidebar Block renders for confidential = false and i
<div
class="no-value sidebar-item-value"
data-testid="not-confidential"
>
<icon-stub
aria-hidden="true"
@ -115,6 +119,7 @@ exports[`Confidential Issue Sidebar Block renders for confidential = false and i
exports[`Confidential Issue Sidebar Block renders for confidential = true and isEditable = false 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
iid=""
>
<div
class="sidebar-collapsed-icon"
@ -167,6 +172,7 @@ exports[`Confidential Issue Sidebar Block renders for confidential = true and is
exports[`Confidential Issue Sidebar Block renders for confidential = true and isEditable = true 1`] = `
<div
class="block issuable-sidebar-item confidentiality"
iid=""
>
<div
class="sidebar-collapsed-icon"

View File

@ -1,16 +1,49 @@
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import EditFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
import eventHub from '~/sidebar/event_hub';
import createStore from '~/notes/stores';
import waitForPromises from 'helpers/wait_for_promises';
import flash from '~/flash';
jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() }));
jest.mock('~/flash');
describe('Edit Form Buttons', () => {
let wrapper;
let store;
const findConfidentialToggle = () => wrapper.find('[data-testid="confidential-toggle"]');
const createComponent = props => {
const createComponent = ({
props = {},
data = {},
confidentialApolloSidebar = false,
resolved = true,
}) => {
store = createStore();
if (resolved) {
jest.spyOn(store, 'dispatch').mockResolvedValue();
} else {
jest.spyOn(store, 'dispatch').mockRejectedValue();
}
wrapper = shallowMount(EditFormButtons, {
propsData: {
updateConfidentialAttribute: () => {},
fullPath: '',
...props,
},
data() {
return {
isLoading: true,
...data,
};
},
provide: {
glFeatures: {
confidentialApolloSidebar,
},
},
store,
});
};
@ -19,10 +52,32 @@ describe('Edit Form Buttons', () => {
wrapper = null;
});
describe('when isLoading', () => {
beforeEach(() => {
createComponent({});
wrapper.vm.$store.state.noteableData.confidential = false;
});
it('renders "Applying" in the toggle button', () => {
expect(findConfidentialToggle().text()).toBe('Applying');
});
it('disables the toggle button', () => {
expect(findConfidentialToggle().attributes('disabled')).toBe('disabled');
});
it('finds the GlLoadingIcon', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
describe('when not confidential', () => {
it('renders Turn On in the ', () => {
it('renders Turn On in the toggle button', () => {
createComponent({
isConfidential: false,
data: {
isLoading: false,
},
});
expect(findConfidentialToggle().text()).toBe('Turn On');
@ -30,12 +85,75 @@ describe('Edit Form Buttons', () => {
});
describe('when confidential', () => {
it('renders on or off text based on confidentiality', () => {
beforeEach(() => {
createComponent({
isConfidential: true,
data: {
isLoading: false,
},
});
wrapper.vm.$store.state.noteableData.confidential = true;
});
it('renders on or off text based on confidentiality', () => {
expect(findConfidentialToggle().text()).toBe('Turn Off');
});
describe('when clicking on the confidential toggle', () => {
it('emits updateConfidentialAttribute', () => {
findConfidentialToggle().trigger('click');
expect(eventHub.$emit).toHaveBeenCalledWith('updateConfidentialAttribute');
});
});
});
describe('when confidentialApolloSidebar is turned on', () => {
const isConfidential = true;
describe('when succeeds', () => {
beforeEach(() => {
createComponent({ data: { isLoading: false }, confidentialApolloSidebar: true });
wrapper.vm.$store.state.noteableData.confidential = isConfidential;
findConfidentialToggle().trigger('click');
});
it('dispatches the correct action', () => {
expect(store.dispatch).toHaveBeenCalledWith('updateConfidentialityOnIssue', {
confidential: !isConfidential,
fullPath: '',
});
});
it('resets loading', () => {
return waitForPromises().then(() => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
});
it('emits close form', () => {
return waitForPromises().then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('closeConfidentialityForm');
});
});
});
describe('when fails', () => {
beforeEach(() => {
createComponent({
data: { isLoading: false },
confidentialApolloSidebar: true,
resolved: false,
});
wrapper.vm.$store.state.noteableData.confidential = isConfidential;
findConfidentialToggle().trigger('click');
});
it('calls flash with the correct message', () => {
expect(flash).toHaveBeenCalledWith(
'Something went wrong trying to change the confidentiality of this issue',
);
});
});
});
});

View File

@ -10,6 +10,8 @@ describe('Edit Form Dropdown', () => {
wrapper = shallowMount(EditForm, {
propsData: {
...props,
isLoading: false,
fullPath: '',
},
});
};

View File

@ -7,6 +7,7 @@ import createFlash from '~/flash';
import RecaptchaModal from '~/vue_shared/components/recaptcha_modal.vue';
import createStore from '~/notes/stores';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import eventHub from '~/sidebar/event_hub';
jest.mock('~/flash');
jest.mock('~/sidebar/services/sidebar_service');
@ -15,6 +16,9 @@ describe('Confidential Issue Sidebar Block', () => {
useMockLocationHelper();
let wrapper;
const mutate = jest
.fn()
.mockResolvedValue({ data: { issueSetConfidential: { issue: { confidential: true } } } });
const findRecaptchaModal = () => wrapper.find(RecaptchaModal);
@ -25,24 +29,32 @@ describe('Confidential Issue Sidebar Block', () => {
wrapper.vm
.$nextTick()
.then(() => {
const editForm = wrapper.find(EditForm);
const { updateConfidentialAttribute } = editForm.props();
updateConfidentialAttribute();
eventHub.$emit('updateConfidentialAttribute');
})
// wait for reCAPTCHA modal to render
.then(() => wrapper.vm.$nextTick())
);
};
const createComponent = propsData => {
const createComponent = ({ propsData, data = {} }) => {
const store = createStore();
const service = new SidebarService();
wrapper = shallowMount(ConfidentialIssueSidebar, {
store,
data() {
return data;
},
propsData: {
service,
iid: '',
fullPath: '',
...propsData,
},
mocks: {
$apollo: {
mutate,
},
},
});
};
@ -60,7 +72,9 @@ describe('Confidential Issue Sidebar Block', () => {
'renders for confidential = $confidential and isEditable = $isEditable',
({ confidential, isEditable }) => {
createComponent({
isEditable,
propsData: {
isEditable,
},
});
wrapper.vm.$store.state.noteableData.confidential = confidential;
@ -73,7 +87,9 @@ describe('Confidential Issue Sidebar Block', () => {
describe('if editable', () => {
beforeEach(() => {
createComponent({
isEditable: true,
propsData: {
isEditable: true,
},
});
wrapper.vm.$store.state.noteableData.confidential = true;
});

View File

@ -13,34 +13,62 @@ RSpec.describe Gitlab::AlertManagement::Fingerprint do
context 'when data is an array' do
let(:data) { [1, 'fingerprint', 'given'] }
it 'flattens the array' do
expect_next_instance_of(described_class) do |obj|
expect(obj).to receive(:flatten_array)
end
subject
end
it 'returns the hashed fingerprint' do
expected_fingerprint = Digest::SHA1.hexdigest(data.flatten.map!(&:to_s).join)
expect(subject).to eq(expected_fingerprint)
end
context 'with a variety of data' do
where(:data) do
[
111,
'fingerprint',
:fingerprint,
true
]
end
with_them do
it 'performs like a hashed fingerprint' do
expect(subject).to eq(Digest::SHA1.hexdigest(data.to_s))
end
end
end
end
context 'when data is a non-array type' do
where(:data) do
[
111,
'fingerprint',
:fingerprint,
true,
{ test: true }
]
context 'when data is a hash' do
let(:data) { { test: true } }
shared_examples 'fingerprinted Hash' do
it 'performs like a hashed fingerprint' do
flattened_hash = Gitlab::Utils::SafeInlineHash.merge_keys!(data).sort.to_s
expect(subject).to eq(Digest::SHA1.hexdigest(flattened_hash))
end
end
with_them do
it 'performs like a hashed fingerprint' do
expect(subject).to eq(Digest::SHA1.hexdigest(data.to_s))
it_behaves_like 'fingerprinted Hash'
context 'hashes with different order' do
it 'calculates the same result' do
data = { test: true, another_test: 1 }
data_hash = described_class.generate(data)
reverse_data = { another_test: 1, test: true }
reverse_data_hash = described_class.generate(reverse_data)
expect(data_hash).to eq(reverse_data_hash)
end
end
context 'hash is too large' do
before do
expect_next_instance_of(Gitlab::Utils::SafeInlineHash) do |obj|
expect(obj).to receive(:valid?).and_return(false)
end
end
it 'raises an error' do
expect { subject }.to raise_error(ArgumentError)
end
end
end

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Alerting::NotificationPayloadParser do
let_it_be(:project) { build(:project) }
describe '.call' do
let(:starts_at) { Time.current.change(usec: 0) }
let(:payload) do
@ -17,7 +19,7 @@ RSpec.describe Gitlab::Alerting::NotificationPayloadParser do
}
end
subject { described_class.call(payload) }
subject { described_class.call(payload, project) }
it 'returns Prometheus-like payload' do
is_expected.to eq(

View File

@ -5,7 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
let(:project) { create(:project, ci_config_path: ci_config_path) }
let(:pipeline) { build(:ci_pipeline, project: project) }
let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project) }
let(:content) { nil }
let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project, content: content) }
subject { described_class.new(pipeline, command) }
@ -141,6 +142,25 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do
end
end
context 'when config is passed as a parameter' do
let(:ci_config_path) { nil }
let(:content) do
<<~EOY
---
stages:
- dast
EOY
end
it 'uses the parameter content' do
subject.perform!
expect(pipeline.config_source).to eq 'parameter_source'
expect(pipeline.pipeline_config.content).to eq(content)
expect(command.config_content).to eq(content)
end
end
context 'when config is not defined anywhere' do
let(:ci_config_path) { nil }

View File

@ -1923,6 +1923,37 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
context 'when there are two pipelines for a ref, sha across multiple projects' do
let(:project_2) { create(:project) }
let!(:commit_456_project_2_ref_test) do
create(
:ci_empty_pipeline,
status: 'success',
ref: 'test',
sha: '456',
project: project_2
)
end
context 'when project_key is false' do
it 'returns the latest pipeline' do
result = described_class.latest_pipeline_per_commit(%w[456])
expect(result).to match('456' => commit_456_project_2_ref_test)
end
end
context 'when project_key is true' do
it 'returns the latest pipeline per project' do
result = described_class.latest_pipeline_per_commit(%w[456], project_key: true)
expect(result[project.id]).to match('456' => commit_456_ref_test)
expect(result[project_2.id]).to match('456' => commit_456_project_2_ref_test)
end
end
end
context 'with a ref' do
it 'only includes the pipelines for the given ref' do
result = described_class.latest_pipeline_per_commit(%w[123 456], 'master')
@ -1947,6 +1978,23 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
describe '.last_finished_for_ref_id' do
let(:project) { create(:project, :repository) }
let(:branch) { project.default_branch }
let(:ref) { project.ci_refs.take }
let(:config_source) { Ci::PipelineEnums.config_sources[:parameter_source] }
let!(:pipeline1) { create(:ci_pipeline, :success, project: project, ref: branch) }
let!(:pipeline2) { create(:ci_pipeline, :success, project: project, ref: branch) }
let!(:pipeline3) { create(:ci_pipeline, :failed, project: project, ref: branch) }
let!(:pipeline4) { create(:ci_pipeline, :success, project: project, ref: branch) }
let!(:pipeline5) { create(:ci_pipeline, :success, project: project, ref: branch, config_source: config_source) }
it 'returns the expected pipeline' do
result = described_class.last_finished_for_ref_id(ref.id)
expect(result).to eq(pipeline4)
end
end
describe '.internal_sources' do
subject { described_class.internal_sources }

View File

@ -62,6 +62,35 @@ RSpec.describe Ci::Ref do
end
end
describe '#last_finished_pipeline_id' do
let(:pipeline_status) { :running }
let(:config_source) { Ci::PipelineEnums.config_sources[:repository_source] }
let(:pipeline) { create(:ci_pipeline, pipeline_status, config_source: config_source) }
let(:ci_ref) { pipeline.ci_ref }
context 'when there are no finished pipelines' do
it 'returns nil' do
expect(ci_ref.last_finished_pipeline_id).to be_nil
end
end
context 'when there are finished pipelines' do
let(:pipeline_status) { :success }
it 'returns the pipeline id' do
expect(ci_ref.last_finished_pipeline_id).to eq(pipeline.id)
end
context 'when the pipeline is not a ci_source' do
let(:config_source) { Ci::PipelineEnums.config_sources[:parameter_source] }
it 'returns nil' do
expect(ci_ref.last_finished_pipeline_id).to be_nil
end
end
end
end
describe '#update_status_by!' do
subject { ci_ref.update_status_by!(pipeline) }

View File

@ -1651,6 +1651,14 @@ RSpec.describe Project do
let(:project_name) { 'group.example.com' }
it { is_expected.to eq("http://group.example.com") }
context 'mixed case path' do
before do
project.update!(path: 'Group.example.com')
end
it { is_expected.to eq("http://group.example.com") }
end
end
context 'project page' do

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'spec_helper'
describe Ci::CreatePipelineService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:admin) }
let(:service) { described_class.new(project, user, { ref: 'refs/heads/master' }) }
let(:content) do
<<~EOY
---
stages:
- dast
variables:
DAST_VERSION: 1
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
dast:
stage: dast
image:
name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION"
variables:
GIT_STRATEGY: none
script:
- /analyze
EOY
end
describe '#execute' do
subject { service.execute(:web, content: content) }
context 'parameter config content' do
it 'creates a pipeline' do
expect(subject).to be_persisted
end
it 'creates builds with the correct names' do
expect(subject.builds.pluck(:name)).to match_array %w[dast]
end
it 'creates stages with the correct names' do
expect(subject.stages.pluck(:name)).to match_array %w[dast]
end
it 'sets the correct config source' do
expect(subject.config_source).to eq 'parameter_source'
end
end
end
end

View File

@ -843,15 +843,15 @@
eslint-plugin-vue "^6.2.1"
vue-eslint-parser "^7.0.0"
"@gitlab/svgs@1.141.0":
version "1.141.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.141.0.tgz#0d6c03511180669538be5c63a96b2ae28840bbf4"
integrity sha512-6k4HA0jVGMb/47bqcflSdpLGpo0rN2yd5K2X39LVQxukrg56PdZQvFPxT2UDOgChLstEtmN/iJTZuXqpeVOg+g==
"@gitlab/svgs@1.143.0":
version "1.143.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.143.0.tgz#94bb9c09dd328ea7c125b21349f7237e9d6786b8"
integrity sha512-KZSMfR2DWT4EeEMGopbFOpqK1F2X9BXdUpUaLp8ovOmFJ1mtiMxNk9QYkejhx91IQrc8wwYp/XPPSeGOZjjCnA==
"@gitlab/ui@17.2.0":
version "17.2.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.2.0.tgz#c4bca963c987f21131be0a650bca47b3708714f2"
integrity sha512-pCzHoA41ggaPjN7612I5MxXq370Utlml9joUuo92BAXQk5XslAJQOFkdmyF/E89qaT7vgWurVAfPylqnSc5LrA==
"@gitlab/ui@17.6.1":
version "17.6.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-17.6.1.tgz#3c7cd6463a0d38c7c5037b5ca861af99680df9ae"
integrity sha512-iCgcKn8hxn2k/763t77CRf07Dnw7wycl+rCUN8UIzBKe4yNswaNVrvjcdfM/TXe46EUVkQ2LfrwPuBShXAeHCA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"