Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a928c5170f
commit
08b3b98051
|
@ -94,5 +94,6 @@ webpack-dev-server.json
|
|||
.solargraph.yml
|
||||
apollo.config.js
|
||||
/tmp/matching_foss_tests.txt
|
||||
/tmp/matching_tests.txt
|
||||
ee/changelogs/unreleased-ee
|
||||
|
||||
|
|
|
@ -493,19 +493,17 @@ rspec-ee system pg12 geo:
|
|||
rspec foss-impact:
|
||||
extends:
|
||||
- .rspec-base-pg11-as-if-foss
|
||||
- .rails:rules:ee-mr-only
|
||||
- .rails:rules:rspec-foss-impact
|
||||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets as-if-foss", "detect-tests as-if-foss"]
|
||||
script:
|
||||
- install_gitlab_gem
|
||||
- install_tff_gem
|
||||
- run_timed_command "scripts/gitaly-test-build"
|
||||
- run_timed_command "scripts/gitaly-test-spawn"
|
||||
- source scripts/rspec_helpers.sh
|
||||
- tooling/bin/find_foss_tests tmp/matching_foss_tests.txt
|
||||
- rspec_matched_foss_tests tmp/matching_foss_tests.txt "--tag ~quarantine"
|
||||
artifacts:
|
||||
expire_in: 7d
|
||||
paths:
|
||||
- tmp/matching_foss_tests.txt
|
||||
- tmp/capybara/
|
||||
|
||||
# EE: Canonical MR pipelines
|
||||
##################################################
|
||||
|
|
|
@ -555,7 +555,16 @@
|
|||
- <<: *if-master-refs
|
||||
changes: *code-backstage-patterns
|
||||
|
||||
.rails:rules:ee-mr-only:
|
||||
.rails:rules:detect-tests:
|
||||
rules:
|
||||
- <<: *if-not-ee
|
||||
when: never
|
||||
- <<: *if-security-merge-request
|
||||
changes: *code-backstage-patterns
|
||||
- <<: *if-dot-com-gitlab-org-merge-request
|
||||
changes: *code-backstage-patterns
|
||||
|
||||
.rails:rules:rspec-foss-impact:
|
||||
rules:
|
||||
- <<: *if-not-ee
|
||||
when: never
|
||||
|
|
|
@ -59,3 +59,34 @@ verify-tests-yml:
|
|||
- source scripts/utils.sh
|
||||
- install_tff_gem
|
||||
- scripts/verify-tff-mapping
|
||||
|
||||
.detect-test-base:
|
||||
image: ruby:2.6-alpine
|
||||
needs: []
|
||||
stage: prepare
|
||||
script:
|
||||
- source scripts/utils.sh
|
||||
- install_gitlab_gem
|
||||
- install_tff_gem
|
||||
- tooling/bin/find_foss_tests ${MATCHED_TESTS_FILE}
|
||||
artifacts:
|
||||
expire_in: 7d
|
||||
paths:
|
||||
- ${MATCHED_TESTS_FILE}
|
||||
|
||||
detect-tests:
|
||||
extends:
|
||||
- .detect-test-base
|
||||
- .rails:rules:detect-tests
|
||||
variables:
|
||||
MATCHED_TESTS_FILE: tmp/matching_tests.txt
|
||||
|
||||
detect-tests as-if-foss:
|
||||
extends:
|
||||
- .detect-test-base
|
||||
- .rails:rules:detect-tests
|
||||
- .as-if-foss
|
||||
variables:
|
||||
MATCHED_TESTS_FILE: tmp/matching_foss_tests.txt
|
||||
before_script:
|
||||
- '[ "$FOSS_ONLY" = "1" ] && rm -rf ee/ qa/spec/ee/ qa/qa/specs/features/ee/ qa/qa/ee/ qa/qa/ee.rb'
|
||||
|
|
|
@ -116,10 +116,12 @@ linters:
|
|||
- "app/views/import/bitbucket/status.html.haml"
|
||||
- "app/views/import/bitbucket_server/status.html.haml"
|
||||
- "app/views/invites/show.html.haml"
|
||||
- "app/views/jira_connect/subscriptions/index.html.haml"
|
||||
- "app/views/layouts/_mailer.html.haml"
|
||||
- "app/views/layouts/experiment_mailer.html.haml"
|
||||
- "app/views/layouts/header/_default.html.haml"
|
||||
- "app/views/layouts/header/_new_dropdown.haml"
|
||||
- "app/views/layouts/jira_connect.html.haml"
|
||||
- "app/views/layouts/notify.html.haml"
|
||||
- "app/views/notify/_failed_builds.html.haml"
|
||||
- "app/views/notify/_reassigned_issuable_email.html.haml"
|
||||
|
@ -333,8 +335,6 @@ linters:
|
|||
- "ee/app/views/groups/group_members/_sync_button.html.haml"
|
||||
- "ee/app/views/groups/hooks/edit.html.haml"
|
||||
- "ee/app/views/groups/ldap_group_links/index.html.haml"
|
||||
- "ee/app/views/jira_connect/subscriptions/index.html.haml"
|
||||
- "ee/app/views/layouts/jira_connect.html.haml"
|
||||
- "ee/app/views/layouts/nav/ee/admin/_new_monitoring_sidebar.html.haml"
|
||||
- "ee/app/views/layouts/service_desk.html.haml"
|
||||
- "ee/app/views/ldap_group_links/_form.html.haml"
|
||||
|
|
|
@ -1 +1 @@
|
|||
e9860f7988a2c87638abf695d8613e3096312857
|
||||
851da3925944b969da7f87057ba8da8274d5c18d
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { GlNewDropdown, GlNewDropdownItem, GlLink } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { defaultIntegrationLevel, overrideDropdownDescriptions } from '../constants';
|
||||
|
||||
const dropdownOptions = [
|
||||
{
|
||||
|
@ -41,6 +43,16 @@ export default {
|
|||
selected: dropdownOptions.find(x => x.value === this.override),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(['adminState']),
|
||||
description() {
|
||||
const level = this.adminState.integrationLevel;
|
||||
|
||||
return (
|
||||
overrideDropdownDescriptions[level] || overrideDropdownDescriptions[defaultIntegrationLevel]
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick(option) {
|
||||
this.selected = option;
|
||||
|
@ -55,7 +67,7 @@ export default {
|
|||
class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline gl-py-4 gl-mt-5 gl-mb-6 gl-border-t-1 gl-border-t-solid gl-border-b-1 gl-border-b-solid gl-border-gray-100"
|
||||
>
|
||||
<span
|
||||
>{{ s__('Integrations|Default settings are inherited from the instance level.') }}
|
||||
>{{ description }}
|
||||
<gl-link v-if="learnMorePath" :href="learnMorePath" target="_blank">{{
|
||||
__('Learn more')
|
||||
}}</gl-link>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export const integrationLevels = {
|
||||
GROUP: 'group',
|
||||
INSTANCE: 'instance',
|
||||
};
|
||||
|
||||
export const defaultIntegrationLevel = integrationLevels.INSTANCE;
|
||||
|
||||
export const overrideDropdownDescriptions = {
|
||||
[integrationLevels.GROUP]: s__(
|
||||
'Integrations|Default settings are inherited from the group level.',
|
||||
),
|
||||
[integrationLevels.INSTANCE]: s__(
|
||||
'Integrations|Default settings are inherited from the instance level.',
|
||||
),
|
||||
};
|
|
@ -23,6 +23,7 @@ function parseDatasetToProps(data) {
|
|||
triggerEvents,
|
||||
fields,
|
||||
inheritFromId,
|
||||
integrationLevel,
|
||||
...booleanAttributes
|
||||
} = data;
|
||||
const {
|
||||
|
@ -56,6 +57,7 @@ function parseDatasetToProps(data) {
|
|||
triggerEvents: JSON.parse(triggerEvents),
|
||||
fields: JSON.parse(fields),
|
||||
inheritFromId: parseInt(inheritFromId, 10),
|
||||
integrationLevel,
|
||||
id: parseInt(id, 10),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/* eslint-disable func-names, no-var, no-alert */
|
||||
/* global $ */
|
||||
/* global AP */
|
||||
|
||||
/**
|
||||
* This script is not going through Webpack bundling
|
||||
* as it is only included in `app/views/jira_connect/subscriptions/index.html.haml`
|
||||
* which is going to be rendered within iframe on Jira app dashboard
|
||||
* hence any code written here needs to be IE11+ compatible (no fully ES6)
|
||||
*/
|
||||
|
||||
function onLoaded() {
|
||||
var reqComplete = function() {
|
||||
AP.navigator.reload();
|
||||
};
|
||||
|
||||
var reqFailed = function(res) {
|
||||
alert(res.responseJSON.error);
|
||||
};
|
||||
|
||||
$('#add-subscription-form').on('submit', function(e) {
|
||||
var actionUrl = $(this).attr('action');
|
||||
e.preventDefault();
|
||||
|
||||
AP.context.getToken(function(token) {
|
||||
// eslint-disable-next-line no-jquery/no-ajax
|
||||
$.post(actionUrl, {
|
||||
jwt: token,
|
||||
namespace_path: $('#namespace-input').val(),
|
||||
format: 'json',
|
||||
})
|
||||
.done(reqComplete)
|
||||
.fail(reqFailed);
|
||||
});
|
||||
});
|
||||
|
||||
$('.remove-subscription').on('click', function(e) {
|
||||
var href = $(this).attr('href');
|
||||
e.preventDefault();
|
||||
|
||||
AP.context.getToken(function(token) {
|
||||
// eslint-disable-next-line no-jquery/no-ajax
|
||||
$.ajax({
|
||||
url: href,
|
||||
method: 'DELETE',
|
||||
data: {
|
||||
jwt: token,
|
||||
format: 'json',
|
||||
},
|
||||
})
|
||||
.done(reqComplete)
|
||||
.fail(reqFailed);
|
||||
});
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', onLoaded);
|
|
@ -303,7 +303,41 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo
|
|||
});
|
||||
}
|
||||
|
||||
/* eslint-disable @gitlab/require-i18n-strings */
|
||||
export function keypressNoteText(e) {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
return;
|
||||
}
|
||||
const keys = {
|
||||
'*': '**{text}**', // wraps with bold character
|
||||
_: '_{text}_', // wraps with italic character
|
||||
'`': '`{text}`', // wraps with inline character
|
||||
"'": "'{text}'", // single quotes
|
||||
'"': '"{text}"', // double quotes
|
||||
'[': '[{text}]', // brackets
|
||||
'{': '{{text}}', // braces
|
||||
'(': '({text})', // parentheses
|
||||
'<': '<{text}>', // angle brackets
|
||||
};
|
||||
const tag = keys[e.key];
|
||||
|
||||
if (tag) {
|
||||
e.preventDefault();
|
||||
|
||||
updateText({
|
||||
tag,
|
||||
textArea: this,
|
||||
blockTag: '',
|
||||
wrap: true,
|
||||
select: '',
|
||||
tagContent: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
/* eslint-enable @gitlab/require-i18n-strings */
|
||||
|
||||
export function addMarkdownListeners(form) {
|
||||
$('.markdown-area', form).on('keydown', keypressNoteText);
|
||||
return $('.js-md', form)
|
||||
.off('click')
|
||||
.on('click', function() {
|
||||
|
@ -342,5 +376,6 @@ export function addEditorMarkdownListeners(editor) {
|
|||
}
|
||||
|
||||
export function removeMarkdownListeners(form) {
|
||||
$('.markdown-area', form).off('keydown', keypressNoteText);
|
||||
return $('.js-md', form).off('click');
|
||||
}
|
||||
|
|
|
@ -423,27 +423,28 @@ export default {
|
|||
<div
|
||||
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
|
||||
>
|
||||
<button
|
||||
<gl-button
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button"
|
||||
class="js-comment-button js-comment-submit-button qa-comment-button"
|
||||
type="submit"
|
||||
category="primary"
|
||||
variant="success"
|
||||
:data-track-label="trackingLabel"
|
||||
data-track-event="click_button"
|
||||
@click.prevent="handleSave()"
|
||||
>{{ commentButtonTitle }}</gl-button
|
||||
>
|
||||
{{ commentButtonTitle }}
|
||||
</button>
|
||||
<button
|
||||
<gl-button
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
name="button"
|
||||
type="button"
|
||||
class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
|
||||
category="primary"
|
||||
variant="success"
|
||||
class="note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
|
||||
data-display="static"
|
||||
data-toggle="dropdown"
|
||||
icon="chevron-down"
|
||||
:aria-label="__('Open comment type dropdown')"
|
||||
>
|
||||
<i aria-hidden="true" class="fa fa-caret-down toggle-icon"></i>
|
||||
</button>
|
||||
/>
|
||||
|
||||
<ul class="note-type-dropdown dropdown-open-top dropdown-menu">
|
||||
<li :class="{ 'droplab-item-selected': noteType === 'comment' }">
|
||||
|
@ -467,11 +468,7 @@ export default {
|
|||
</li>
|
||||
<li class="divider droplab-item-ignore"></li>
|
||||
<li :class="{ 'droplab-item-selected': noteType === 'discussion' }">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent qa-discussion-option"
|
||||
@click.prevent="setNoteType('discussion')"
|
||||
>
|
||||
<button class="qa-discussion-option" @click.prevent="setNoteType('discussion')">
|
||||
<i aria-hidden="true" class="fa fa-check icon"></i>
|
||||
<div class="description">
|
||||
<strong>{{ __('Start thread') }}</strong>
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
<script>
|
||||
import { GlDeprecatedButton, GlProgressBar, GlIcon } from '@gitlab/ui';
|
||||
import { GlButton, GlProgressBar } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { formattedTime } from '../../stores/test_reports/utils';
|
||||
|
||||
export default {
|
||||
name: 'TestSummary',
|
||||
components: {
|
||||
GlDeprecatedButton,
|
||||
GlButton,
|
||||
GlProgressBar,
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
report: {
|
||||
|
@ -68,14 +67,13 @@ export default {
|
|||
<div>
|
||||
<div class="row">
|
||||
<div class="col-12 d-flex gl-mt-3 align-items-center">
|
||||
<gl-deprecated-button
|
||||
<gl-button
|
||||
v-if="showBack"
|
||||
size="sm"
|
||||
size="small"
|
||||
class="gl-mr-3 js-back-button"
|
||||
icon="angle-left"
|
||||
@click="onBackClick"
|
||||
>
|
||||
<gl-icon name="angle-left" />
|
||||
</gl-deprecated-button>
|
||||
/>
|
||||
|
||||
<h4>{{ heading }}</h4>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
@import 'framework/variables';
|
||||
|
||||
$atlaskit-border-color: #dfe1e6;
|
||||
|
||||
.ac-content {
|
||||
margin: 20px;
|
||||
|
||||
.subscription-form {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.field-group-input {
|
||||
display: flex;
|
||||
padding-top: $gl-padding-4;
|
||||
|
||||
.ak-button {
|
||||
height: auto;
|
||||
margin-left: $btn-margin-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subscriptions {
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid $atlaskit-border-color;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: $gl-padding-8;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,12 @@ module Groups
|
|||
@integrations = Service.find_or_initialize_all(Service.for_group(group)).sort_by(&:title)
|
||||
end
|
||||
|
||||
def edit
|
||||
@admin_integration = Service.instance_for(integration.type)
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_or_initialize_integration(name)
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This returns an app descriptor for use with Jira in development mode
|
||||
# For the Atlassian Marketplace, a static copy of this JSON is uploaded to the marketplace
|
||||
# https://developer.atlassian.com/cloud/jira/platform/app-descriptor/
|
||||
|
||||
class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
|
||||
skip_before_action :verify_atlassian_jwt!
|
||||
|
||||
def show
|
||||
render json: {
|
||||
name: Atlassian::JiraConnect.app_name,
|
||||
description: 'Integrate commits, branches and merge requests from GitLab into Jira',
|
||||
key: Atlassian::JiraConnect.app_key,
|
||||
baseUrl: jira_connect_base_url(protocol: 'https'),
|
||||
lifecycle: {
|
||||
installed: relative_to_base_path(jira_connect_events_installed_path),
|
||||
uninstalled: relative_to_base_path(jira_connect_events_uninstalled_path)
|
||||
},
|
||||
vendor: {
|
||||
name: 'GitLab',
|
||||
url: 'https://gitlab.com'
|
||||
},
|
||||
links: {
|
||||
documentation: help_page_url('integration/jira_development_panel', anchor: 'gitlabcom-1')
|
||||
},
|
||||
authentication: {
|
||||
type: 'jwt'
|
||||
},
|
||||
scopes: %w(READ WRITE DELETE),
|
||||
apiVersion: 1,
|
||||
modules: {
|
||||
jiraDevelopmentTool: {
|
||||
key: 'gitlab-development-tool',
|
||||
application: {
|
||||
value: 'GitLab'
|
||||
},
|
||||
name: {
|
||||
value: 'GitLab'
|
||||
},
|
||||
url: 'https://gitlab.com',
|
||||
logoUrl: view_context.image_url('gitlab_logo.png'),
|
||||
capabilities: %w(branch commit pull_request)
|
||||
},
|
||||
postInstallPage: {
|
||||
key: 'gitlab-configuration',
|
||||
name: {
|
||||
value: 'GitLab Configuration'
|
||||
},
|
||||
url: relative_to_base_path(jira_connect_subscriptions_path)
|
||||
}
|
||||
},
|
||||
apiMigrations: {
|
||||
gdpr: true
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def relative_to_base_path(full_path)
|
||||
full_path.sub(/^#{jira_connect_base_path}/, '')
|
||||
end
|
||||
end
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class JiraConnect::ApplicationController < ApplicationController
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
skip_before_action :authenticate_user!
|
||||
skip_before_action :verify_authenticity_token
|
||||
before_action :verify_atlassian_jwt!
|
||||
|
||||
attr_reader :current_jira_installation
|
||||
|
||||
private
|
||||
|
||||
def verify_atlassian_jwt!
|
||||
return render_403 unless atlassian_jwt_valid?
|
||||
|
||||
@current_jira_installation = installation_from_jwt
|
||||
end
|
||||
|
||||
def verify_qsh_claim!
|
||||
payload, _ = decode_auth_token!
|
||||
|
||||
# Make sure `qsh` claim matches the current request
|
||||
render_403 unless payload['qsh'] == Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url)
|
||||
rescue
|
||||
render_403
|
||||
end
|
||||
|
||||
def atlassian_jwt_valid?
|
||||
return false unless installation_from_jwt
|
||||
|
||||
# Verify JWT signature with our stored `shared_secret`
|
||||
decode_auth_token!
|
||||
rescue JWT::DecodeError
|
||||
false
|
||||
end
|
||||
|
||||
def installation_from_jwt
|
||||
return unless auth_token
|
||||
|
||||
strong_memoize(:installation_from_jwt) do
|
||||
# Decode without verification to get `client_key` in `iss`
|
||||
payload, _ = Atlassian::Jwt.decode(auth_token, nil, false)
|
||||
JiraConnectInstallation.find_by_client_key(payload['iss'])
|
||||
end
|
||||
end
|
||||
|
||||
def decode_auth_token!
|
||||
Atlassian::Jwt.decode(auth_token, installation_from_jwt.shared_secret)
|
||||
end
|
||||
|
||||
def auth_token
|
||||
strong_memoize(:auth_token) do
|
||||
params[:jwt] || request.headers['Authorization']&.split(' ', 2)&.last
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class JiraConnect::EventsController < JiraConnect::ApplicationController
|
||||
skip_before_action :verify_atlassian_jwt!, only: :installed
|
||||
before_action :verify_qsh_claim!, only: :uninstalled
|
||||
|
||||
def installed
|
||||
installation = JiraConnectInstallation.new(install_params)
|
||||
|
||||
if installation.save
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def uninstalled
|
||||
if current_jira_installation.destroy
|
||||
head :ok
|
||||
else
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def install_params
|
||||
params.permit(:clientKey, :sharedSecret, :baseUrl).transform_keys(&:underscore)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
|
||||
layout 'jira_connect'
|
||||
|
||||
content_security_policy do |p|
|
||||
next if p.directives.blank?
|
||||
|
||||
# rubocop: disable Lint/PercentStringArray
|
||||
script_src_values = Array.wrap(p.directives['script-src']) | %w('self' https://connect-cdn.atl-paas.net https://unpkg.com/jquery@3.3.1/)
|
||||
style_src_values = Array.wrap(p.directives['style-src']) | %w('self' 'unsafe-inline' https://unpkg.com/@atlaskit/)
|
||||
# rubocop: enable Lint/PercentStringArray
|
||||
|
||||
p.frame_ancestors :self, 'https://*.atlassian.net'
|
||||
p.script_src(*script_src_values)
|
||||
p.style_src(*style_src_values)
|
||||
end
|
||||
|
||||
before_action :allow_rendering_in_iframe, only: :index
|
||||
before_action :verify_qsh_claim!, only: :index
|
||||
before_action :authenticate_user!, only: :create
|
||||
|
||||
def index
|
||||
@subscriptions = current_jira_installation.subscriptions.preload_namespace_route
|
||||
end
|
||||
|
||||
def create
|
||||
result = create_service.execute
|
||||
|
||||
if result[:status] == :success
|
||||
render json: { success: true }
|
||||
else
|
||||
render json: { error: result[:message] }, status: result[:http_status]
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
subscription = current_jira_installation.subscriptions.find(params[:id])
|
||||
|
||||
if subscription.destroy
|
||||
render json: { success: true }
|
||||
else
|
||||
render json: { error: subscription.errors.full_messages.join(', ') }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_service
|
||||
JiraConnectSubscriptions::CreateService.new(current_jira_installation, current_user, namespace_path: params['namespace_path'])
|
||||
end
|
||||
|
||||
def allow_rendering_in_iframe
|
||||
response.headers.delete('X-Frame-Options')
|
||||
end
|
||||
end
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This controller's role is to mimic and rewire the GitLab OAuth
|
||||
# flow routes for Jira DVCS integration.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/issues/2381
|
||||
#
|
||||
class Oauth::Jira::AuthorizationsController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_before_action :verify_authenticity_token
|
||||
|
||||
# 1. Rewire Jira OAuth initial request to our stablished OAuth authorization URL.
|
||||
def new
|
||||
session[:redirect_uri] = params['redirect_uri']
|
||||
|
||||
redirect_to oauth_authorization_path(client_id: params['client_id'],
|
||||
response_type: 'code',
|
||||
redirect_uri: oauth_jira_callback_url)
|
||||
end
|
||||
|
||||
# 2. Handle the callback call as we were a Github Enterprise instance client.
|
||||
def callback
|
||||
# Handling URI query params concatenation.
|
||||
redirect_uri = URI.parse(session['redirect_uri'])
|
||||
new_query = URI.decode_www_form(String(redirect_uri.query)) << ['code', params[:code]]
|
||||
redirect_uri.query = URI.encode_www_form(new_query)
|
||||
|
||||
redirect_to redirect_uri.to_s
|
||||
end
|
||||
|
||||
# 3. Rewire and adjust access_token request accordingly.
|
||||
def access_token
|
||||
# We have to modify request.parameters because Doorkeeper::Server reads params from there
|
||||
request.parameters[:redirect_uri] = oauth_jira_callback_url
|
||||
|
||||
strategy = Doorkeeper::Server.new(self).token_request('authorization_code')
|
||||
response = strategy.authorize
|
||||
|
||||
if response.status == :ok
|
||||
access_token, scope, token_type = response.body.values_at('access_token', 'scope', 'token_type')
|
||||
|
||||
render body: "access_token=#{access_token}&scope=#{scope}&token_type=#{token_type}"
|
||||
else
|
||||
render status: response.status, body: response.body
|
||||
end
|
||||
rescue Doorkeeper::Errors::DoorkeeperError => e
|
||||
render status: :unauthorized, body: e.type
|
||||
end
|
||||
end
|
|
@ -31,8 +31,10 @@ class PasswordsController < Devise::PasswordsController
|
|||
|
||||
def update
|
||||
super do |resource|
|
||||
if resource.valid? && resource.password_automatically_set?
|
||||
resource.update_attribute(:password_automatically_set, false)
|
||||
if resource.valid?
|
||||
resource.password_automatically_set = false
|
||||
resource.password_expires_at = nil
|
||||
resource.save(validate: false) if resource.changed?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -95,7 +95,8 @@ module ServicesHelper
|
|||
learn_more_path: integrations_help_page_path,
|
||||
trigger_events: trigger_events_for_service(integration),
|
||||
fields: fields_for_service(integration),
|
||||
inherit_from_id: integration.inherit_from_id
|
||||
inherit_from_id: integration.inherit_from_id,
|
||||
integration_level: integration_level(integration)
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -120,6 +121,18 @@ module ServicesHelper
|
|||
end
|
||||
|
||||
extend self
|
||||
|
||||
private
|
||||
|
||||
def integration_level(integration)
|
||||
if integration.instance
|
||||
'instance'
|
||||
elsif integration.group_id
|
||||
'group'
|
||||
else
|
||||
'project'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ServicesHelper.prepend_if_ee('EE::ServicesHelper')
|
||||
|
|
|
@ -161,7 +161,6 @@ module Ci
|
|||
where(file_type: types)
|
||||
end
|
||||
|
||||
scope :expired, -> (limit) { where('expire_at < ?', Time.current).limit(limit) }
|
||||
scope :downloadable, -> { where(file_type: DOWNLOADABLE_TYPES) }
|
||||
scope :unlocked, -> { joins(job: :pipeline).merge(::Ci::Pipeline.unlocked).order(expire_at: :desc) }
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ module Ci
|
|||
zip: 2,
|
||||
gzip: 3
|
||||
}, _suffix: true
|
||||
|
||||
scope :expired, -> (limit) { where('expire_at < ?', Time.current).limit(limit) }
|
||||
end
|
||||
|
||||
def each_blob(&blk)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class JiraConnectInstallation < ApplicationRecord
|
||||
attr_encrypted :shared_secret,
|
||||
mode: :per_attribute_iv,
|
||||
algorithm: 'aes-256-gcm',
|
||||
key: Settings.attr_encrypted_db_key_base_32
|
||||
|
||||
has_many :subscriptions, class_name: 'JiraConnectSubscription'
|
||||
|
||||
validates :client_key, presence: true, uniqueness: true
|
||||
validates :shared_secret, presence: true
|
||||
validates :base_url, presence: true, public_url: true
|
||||
|
||||
scope :for_project, -> (project) {
|
||||
distinct
|
||||
.joins(:subscriptions)
|
||||
.where(jira_connect_subscriptions: {
|
||||
id: JiraConnectSubscription.for_project(project)
|
||||
})
|
||||
}
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class JiraConnectSubscription < ApplicationRecord
|
||||
belongs_to :installation, class_name: 'JiraConnectInstallation', foreign_key: 'jira_connect_installation_id'
|
||||
belongs_to :namespace
|
||||
|
||||
validates :installation, presence: true
|
||||
validates :namespace, presence: true, uniqueness: { scope: :jira_connect_installation_id, message: 'has already been added' }
|
||||
|
||||
scope :preload_namespace_route, -> { preload(namespace: :route) }
|
||||
scope :for_project, -> (project) { where(namespace_id: project.namespace.self_and_ancestors) }
|
||||
end
|
|
@ -254,6 +254,7 @@ class Project < ApplicationRecord
|
|||
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
|
||||
has_one :project_feature, inverse_of: :project
|
||||
has_one :statistics, class_name: 'ProjectStatistics'
|
||||
has_one :feature_usage, class_name: 'ProjectFeatureUsage'
|
||||
|
||||
has_one :cluster_project, class_name: 'Clusters::Project'
|
||||
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
|
||||
|
@ -393,6 +394,8 @@ class Project < ApplicationRecord
|
|||
to: :project_setting
|
||||
delegate :active?, to: :prometheus_service, allow_nil: true, prefix: true
|
||||
|
||||
delegate :log_jira_dvcs_integration_usage, :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage
|
||||
|
||||
# Validations
|
||||
validates :creator, presence: true, on: :create
|
||||
validates :description, length: { maximum: 2000 }, allow_blank: true
|
||||
|
@ -476,6 +479,9 @@ class Project < ApplicationRecord
|
|||
scope :for_milestones, ->(ids) { joins(:milestones).where('milestones.id' => ids).distinct }
|
||||
scope :with_push, -> { joins(:events).merge(Event.pushed_action) }
|
||||
scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') }
|
||||
scope :with_active_jira_services, -> { joins(:services).merge(::JiraService.active) } # rubocop:disable CodeReuse/ServiceClass
|
||||
scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) }
|
||||
scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) }
|
||||
scope :inc_routes, -> { includes(:route, namespace: :route) }
|
||||
scope :with_statistics, -> { includes(:statistics) }
|
||||
scope :with_namespace, -> { includes(:namespace) }
|
||||
|
@ -1444,6 +1450,10 @@ class Project < ApplicationRecord
|
|||
http_url_to_repo
|
||||
end
|
||||
|
||||
def feature_usage
|
||||
super.presence || build_feature_usage
|
||||
end
|
||||
|
||||
def forked?
|
||||
fork_network && fork_network.root_project != self
|
||||
end
|
||||
|
@ -2426,6 +2436,10 @@ class Project < ApplicationRecord
|
|||
false
|
||||
end
|
||||
|
||||
def jira_subscription_exists?
|
||||
JiraConnectSubscription.for_project(self).exists?
|
||||
end
|
||||
|
||||
def uses_default_ci_config?
|
||||
ci_config_path.blank? || ci_config_path == Gitlab::FileDetector::PATTERNS[:gitlab_ci]
|
||||
end
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ProjectFeatureUsage < ApplicationRecord
|
||||
self.primary_key = :project_id
|
||||
|
||||
JIRA_DVCS_CLOUD_FIELD = 'jira_dvcs_cloud_last_sync_at'.freeze
|
||||
JIRA_DVCS_SERVER_FIELD = 'jira_dvcs_server_last_sync_at'.freeze
|
||||
|
||||
belongs_to :project
|
||||
validates :project, presence: true
|
||||
|
||||
scope :with_jira_dvcs_integration_enabled, -> (cloud: true) do
|
||||
where.not(jira_dvcs_integration_field(cloud: cloud) => nil)
|
||||
end
|
||||
|
||||
class << self
|
||||
def jira_dvcs_integration_field(cloud: true)
|
||||
cloud ? JIRA_DVCS_CLOUD_FIELD : JIRA_DVCS_SERVER_FIELD
|
||||
end
|
||||
end
|
||||
|
||||
def log_jira_dvcs_integration_usage(cloud: true)
|
||||
transaction(requires_new: true) do
|
||||
save unless persisted?
|
||||
touch(self.class.jira_dvcs_integration_field(cloud: cloud))
|
||||
end
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
reset
|
||||
retry
|
||||
end
|
||||
end
|
|
@ -116,6 +116,7 @@ class GroupPolicy < BasePolicy
|
|||
enable :update_cluster
|
||||
enable :admin_cluster
|
||||
enable :read_deploy_token
|
||||
enable :create_jira_connect_subscription
|
||||
end
|
||||
|
||||
rule { owner }.policy do
|
||||
|
|
|
@ -12,6 +12,7 @@ class NamespacePolicy < BasePolicy
|
|||
enable :admin_namespace
|
||||
enable :read_namespace
|
||||
enable :read_statistics
|
||||
enable :create_jira_connect_subscription
|
||||
end
|
||||
|
||||
rule { personal_project & ~can_create_personal_project }.prevent :create_projects
|
||||
|
|
|
@ -20,18 +20,18 @@ module Ci
|
|||
def execute
|
||||
in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do
|
||||
loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do
|
||||
destroy_batch
|
||||
destroy_batch(Ci::JobArtifact) || destroy_batch(Ci::PipelineArtifact)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def destroy_batch
|
||||
artifact_batch = if Gitlab::Ci::Features.destroy_only_unlocked_expired_artifacts_enabled?
|
||||
Ci::JobArtifact.expired(BATCH_SIZE).unlocked
|
||||
def destroy_batch(klass)
|
||||
artifact_batch = if klass == Ci::JobArtifact && Gitlab::Ci::Features.destroy_only_unlocked_expired_artifacts_enabled?
|
||||
klass.expired(BATCH_SIZE).unlocked
|
||||
else
|
||||
Ci::JobArtifact.expired(BATCH_SIZE)
|
||||
klass.expired(BATCH_SIZE)
|
||||
end
|
||||
|
||||
artifacts = artifact_batch.to_a
|
||||
|
|
|
@ -75,6 +75,7 @@ module Git
|
|||
|
||||
def branch_change_hooks
|
||||
enqueue_process_commit_messages
|
||||
enqueue_jira_connect_sync_messages
|
||||
end
|
||||
|
||||
def branch_remove_hooks
|
||||
|
@ -103,6 +104,17 @@ module Git
|
|||
end
|
||||
end
|
||||
|
||||
def enqueue_jira_connect_sync_messages
|
||||
return unless project.jira_subscription_exists?
|
||||
|
||||
branch_to_sync = branch_name if Atlassian::JiraIssueKeyExtractor.has_keys?(branch_name)
|
||||
commits_to_sync = limited_commits.select { |commit| Atlassian::JiraIssueKeyExtractor.has_keys?(commit.safe_message) }.map(&:sha)
|
||||
|
||||
if branch_to_sync || commits_to_sync.any?
|
||||
JiraConnect::SyncBranchWorker.perform_async(project.id, branch_to_sync, commits_to_sync)
|
||||
end
|
||||
end
|
||||
|
||||
def unsigned_x509_shas(commits)
|
||||
X509CommitSignature.unsigned_commit_shas(commits.map(&:sha))
|
||||
end
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module JiraConnect
|
||||
class SyncService
|
||||
def initialize(project)
|
||||
self.project = project
|
||||
end
|
||||
|
||||
def execute(commits: nil, branches: nil, merge_requests: nil)
|
||||
JiraConnectInstallation.for_project(project).each do |installation|
|
||||
client = Atlassian::JiraConnect::Client.new(installation.base_url, installation.shared_secret)
|
||||
|
||||
response = client.store_dev_info(project: project, commits: commits, branches: branches, merge_requests: merge_requests)
|
||||
|
||||
log_response(response)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_accessor :project
|
||||
|
||||
def log_response(response)
|
||||
message = {
|
||||
message: 'response from jira dev_info api',
|
||||
integration: 'JiraConnect',
|
||||
project_id: project.id,
|
||||
project_path: project.full_path,
|
||||
jira_response: response&.to_json
|
||||
}
|
||||
|
||||
if response && response['errorMessages']
|
||||
logger.error(message)
|
||||
else
|
||||
logger.info(message)
|
||||
end
|
||||
end
|
||||
|
||||
def logger
|
||||
Gitlab::ProjectServiceLogger
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module JiraConnectSubscriptions
|
||||
class BaseService < ::BaseService
|
||||
attr_accessor :jira_connect_installation, :current_user, :params
|
||||
|
||||
def initialize(jira_connect_installation, user = nil, params = {})
|
||||
@jira_connect_installation, @current_user, @params = jira_connect_installation, user, params.dup
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module JiraConnectSubscriptions
|
||||
class CreateService < ::JiraConnectSubscriptions::BaseService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def execute
|
||||
unless namespace && can?(current_user, :create_jira_connect_subscription, namespace)
|
||||
return error('Invalid namespace. Please make sure you have sufficient permissions', 401)
|
||||
end
|
||||
|
||||
create_subscription
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_subscription
|
||||
subscription = JiraConnectSubscription.new(installation: jira_connect_installation, namespace: namespace)
|
||||
|
||||
if subscription.save
|
||||
success
|
||||
else
|
||||
error(subscription.errors.full_messages.join(', '), 422)
|
||||
end
|
||||
end
|
||||
|
||||
def namespace
|
||||
strong_memoize(:namespace) do
|
||||
Namespace.find_by_full_path(params[:namespace_path])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,6 +23,8 @@ module MergeRequests
|
|||
merge_data = hook_data(merge_request, action, old_rev: old_rev, old_associations: old_associations)
|
||||
merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
|
||||
merge_request.project.execute_services(merge_data, :merge_request_hooks)
|
||||
|
||||
enqueue_jira_connect_messages_for(merge_request)
|
||||
end
|
||||
|
||||
def cleanup_environments(merge_request)
|
||||
|
@ -52,6 +54,14 @@ module MergeRequests
|
|||
|
||||
private
|
||||
|
||||
def enqueue_jira_connect_messages_for(merge_request)
|
||||
return unless project.jira_subscription_exists?
|
||||
|
||||
if Atlassian::JiraIssueKeyExtractor.has_keys?(merge_request.title, merge_request.description)
|
||||
JiraConnect::SyncMergeRequestWorker.perform_async(merge_request.id)
|
||||
end
|
||||
end
|
||||
|
||||
def create(merge_request)
|
||||
self.params = assign_allowed_merge_params(merge_request, params)
|
||||
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
%h1
|
||||
GitLab for Jira Configuration
|
||||
|
||||
%form#add-subscription-form.subscription-form{ action: jira_connect_subscriptions_path }
|
||||
.ak-field-group
|
||||
%label
|
||||
Namespace
|
||||
|
||||
.ak-field-group.field-group-input
|
||||
%input#namespace-input.ak-field-text{ type: 'text', required: true }
|
||||
%button.ak-button.ak-button__appearance-primary{ type: 'submit' }
|
||||
Link namespace to Jira
|
||||
|
||||
%table.subscriptions
|
||||
%thead
|
||||
%tr
|
||||
%th Namespace
|
||||
%th Added
|
||||
%th
|
||||
%tbody
|
||||
- @subscriptions.each do |subscription|
|
||||
%tr
|
||||
%td= subscription.namespace.full_path
|
||||
%td= subscription.created_at
|
||||
%td= link_to 'Remove', jira_connect_subscription_path(subscription), class: 'remove-subscription'
|
||||
|
||||
= page_specific_javascript_tag('jira_connect.js')
|
||||
= stylesheet_link_tag 'page_bundles/jira_connect'
|
|
@ -0,0 +1,13 @@
|
|||
%html{ lang: "en" }
|
||||
%head
|
||||
%meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" }
|
||||
%title
|
||||
GitLab
|
||||
= stylesheet_link_tag 'https://unpkg.com/@atlaskit/css-reset@3.0.6/dist/bundle.css'
|
||||
= stylesheet_link_tag 'https://unpkg.com/@atlaskit/reduced-ui-pack@10.5.5/dist/bundle.css'
|
||||
= javascript_include_tag 'https://connect-cdn.atl-paas.net/all.js'
|
||||
= javascript_include_tag 'https://unpkg.com/jquery@3.3.1/dist/jquery.min.js'
|
||||
= yield :head
|
||||
%body
|
||||
.ac-content
|
||||
= yield
|
|
@ -723,6 +723,22 @@
|
|||
:weight: 2
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: jira_connect:jira_connect_sync_branch
|
||||
:feature_category: :integrations
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: jira_connect:jira_connect_sync_merge_request
|
||||
:feature_category: :integrations
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: jira_importer:jira_import_advance_stage
|
||||
:feature_category: :importers
|
||||
:has_external_dependencies:
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module JiraConnect
|
||||
class SyncBranchWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
|
||||
queue_namespace :jira_connect
|
||||
feature_category :integrations
|
||||
loggable_arguments 1, 2
|
||||
|
||||
def perform(project_id, branch_name, commit_shas)
|
||||
project = Project.find_by_id(project_id)
|
||||
|
||||
return unless project
|
||||
|
||||
branches = [project.repository.find_branch(branch_name)] if branch_name.present?
|
||||
commits = project.commits_by(oids: commit_shas) if commit_shas.present?
|
||||
|
||||
JiraConnect::SyncService.new(project).execute(commits: commits, branches: branches)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module JiraConnect
|
||||
class SyncMergeRequestWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
|
||||
queue_namespace :jira_connect
|
||||
feature_category :integrations
|
||||
|
||||
def perform(merge_request_id)
|
||||
merge_request = MergeRequest.find_by_id(merge_request_id)
|
||||
|
||||
return unless merge_request && merge_request.project
|
||||
|
||||
JiraConnect::SyncService.new(merge_request.project).execute(merge_requests: [merge_request])
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Adjust badge key text and width limits
|
||||
merge_request: 40199
|
||||
author: Fabian Schneider @fabsrc
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove the expiry on user passwords after a user resets their password
|
||||
merge_request: 40712
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Surround selected text in markdown fields on certain key presses
|
||||
merge_request: 37151
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add index for expire_at to ci_pipeline_artifacts
|
||||
merge_request: 39882
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move Jira Development Panel integration to Core
|
||||
merge_request: 40485
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add table for storing user settings for board epic swimlanes
|
||||
merge_request: 40360
|
||||
author:
|
||||
type: added
|
|
@ -178,6 +178,7 @@ module Gitlab
|
|||
config.assets.precompile << "mailers/*.css"
|
||||
config.assets.precompile << "page_bundles/_mixins_and_variables_and_functions.css"
|
||||
config.assets.precompile << "page_bundles/ide.css"
|
||||
config.assets.precompile << "page_bundles/jira_connect.css"
|
||||
config.assets.precompile << "page_bundles/todos.css"
|
||||
config.assets.precompile << "page_bundles/xterm.css"
|
||||
config.assets.precompile << "performance_bar.css"
|
||||
|
@ -187,6 +188,7 @@ module Gitlab
|
|||
config.assets.precompile << "locale/**/app.js"
|
||||
config.assets.precompile << "emoji_sprites.css"
|
||||
config.assets.precompile << "errors.css"
|
||||
config.assets.precompile << "jira_connect.js"
|
||||
|
||||
config.assets.precompile << "highlight/themes/*.css"
|
||||
|
||||
|
@ -205,14 +207,6 @@ module Gitlab
|
|||
config.assets.paths << "#{config.root}/node_modules/xterm/src/"
|
||||
config.assets.precompile << "xterm.css"
|
||||
|
||||
if Gitlab.ee?
|
||||
%w[images javascripts stylesheets].each do |path|
|
||||
config.assets.paths << "#{config.root}/ee/app/assets/#{path}"
|
||||
config.assets.precompile << "jira_connect.js"
|
||||
config.assets.precompile << "pages/jira_connect.css"
|
||||
end
|
||||
end
|
||||
|
||||
# Import path for EE specific SCSS entry point
|
||||
# In CE it will import a noop file, in EE a functioning file
|
||||
# Order is important, so that the ee file takes precedence:
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
|
||||
Feature.register_feature_groups
|
||||
Feature.register_definitions
|
||||
Feature.register_hot_reloader unless Rails.configuration.cache_classes
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
return unless Rails.env.test?
|
||||
|
||||
Rails.application.configure do
|
||||
config.after_initialize do
|
||||
# We don't care about ActiveJob reloading the code in test env as we run
|
||||
# jobs inline in test env.
|
||||
# So in test, we remove this callback, which calls app.reloader.wrap, and
|
||||
# ultimately calls FileUpdateChecker#updated? which is slow on macOS
|
||||
#
|
||||
# https://github.com/rails/rails/blob/6-0-stable/activejob/lib/active_job/railtie.rb#L39-L46
|
||||
def active_job_railtie_callback?
|
||||
callbacks = ActiveJob::Callbacks.singleton_class.__callbacks[:execute]
|
||||
|
||||
callbacks &&
|
||||
callbacks.send(:chain).size == 1 &&
|
||||
callbacks.first.kind == :around &&
|
||||
callbacks.first.raw_filter.is_a?(Proc) &&
|
||||
callbacks.first.raw_filter.source_location.first.ends_with?('lib/active_job/railtie.rb')
|
||||
end
|
||||
|
||||
if active_job_railtie_callback?
|
||||
ActiveJob::Callbacks.singleton_class.reset_callbacks(:execute)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -32,13 +32,10 @@ Rails.application.routes.draw do
|
|||
# This prefixless path is required because Jira gets confused if we set it up with a path
|
||||
# More information: https://gitlab.com/gitlab-org/gitlab/issues/6752
|
||||
scope path: '/login/oauth', controller: 'oauth/jira/authorizations', as: :oauth_jira do
|
||||
Gitlab.ee do
|
||||
get :authorize, action: :new
|
||||
get :callback
|
||||
post :access_token
|
||||
end
|
||||
get :authorize, action: :new
|
||||
get :callback
|
||||
post :access_token
|
||||
|
||||
# This helps minimize merge conflicts with CE for this scope block
|
||||
match '*all', via: [:get, :post], to: proc { [404, {}, ['']] }
|
||||
end
|
||||
|
||||
|
@ -127,11 +124,11 @@ Rails.application.routes.draw do
|
|||
get 'ide/*vueroute' => 'ide#index', format: false
|
||||
|
||||
draw :operations
|
||||
draw :jira_connect
|
||||
|
||||
Gitlab.ee do
|
||||
draw :security
|
||||
draw :smartcard
|
||||
draw :jira_connect
|
||||
draw :username
|
||||
draw :trial
|
||||
draw :trial_registration
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
namespace :jira_connect do
|
||||
# This is so we can have a named route helper for the base URL
|
||||
root to: proc { [404, {}, ['']] }, as: 'base'
|
||||
|
||||
get 'app_descriptor' => 'app_descriptor#show'
|
||||
|
||||
namespace :events do
|
||||
post 'installed'
|
||||
post 'uninstalled'
|
||||
end
|
||||
|
||||
resources :subscriptions, only: [:index, :create, :destroy]
|
||||
end
|
|
@ -564,3 +564,37 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
# rubocop: enable Cop/PutProjectRoutesUnderScope
|
||||
end
|
||||
end
|
||||
|
||||
# It's under /-/jira scope but cop is only checking /-/
|
||||
# rubocop: disable Cop/PutProjectRoutesUnderScope
|
||||
scope path: '(/-/jira)', constraints: ::Constraints::JiraEncodedUrlConstrainer.new, as: :jira do
|
||||
scope path: '*namespace_id/:project_id',
|
||||
namespace_id: Gitlab::Jira::Dvcs::ENCODED_ROUTE_REGEX,
|
||||
project_id: Gitlab::Jira::Dvcs::ENCODED_ROUTE_REGEX do
|
||||
get '/', to: redirect { |params, req|
|
||||
::Gitlab::Jira::Dvcs.restore_full_path(
|
||||
namespace: params[:namespace_id],
|
||||
project: params[:project_id]
|
||||
)
|
||||
}
|
||||
|
||||
get 'commit/:id', constraints: { id: /\h{7,40}/ }, to: redirect { |params, req|
|
||||
project_full_path = ::Gitlab::Jira::Dvcs.restore_full_path(
|
||||
namespace: params[:namespace_id],
|
||||
project: params[:project_id]
|
||||
)
|
||||
|
||||
"/#{project_full_path}/commit/#{params[:id]}"
|
||||
}
|
||||
|
||||
get 'tree/*id', as: nil, to: redirect { |params, req|
|
||||
project_full_path = ::Gitlab::Jira::Dvcs.restore_full_path(
|
||||
namespace: params[:namespace_id],
|
||||
project: params[:project_id]
|
||||
)
|
||||
|
||||
"/#{project_full_path}/-/tree/#{params[:id]}"
|
||||
}
|
||||
end
|
||||
end
|
||||
# rubocop: enable Cop/PutProjectRoutesUnderScope
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BoardsEpicUserPreferences < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
create_table :boards_epic_user_preferences do |t|
|
||||
t.bigint :board_id, null: false
|
||||
t.bigint :user_id, null: false
|
||||
t.bigint :epic_id, null: false
|
||||
t.boolean :collapsed, default: false, null: false
|
||||
end
|
||||
|
||||
add_index :boards_epic_user_preferences, :board_id
|
||||
add_index :boards_epic_user_preferences, :user_id
|
||||
add_index :boards_epic_user_preferences, :epic_id
|
||||
add_index :boards_epic_user_preferences, [:board_id, :user_id, :epic_id], unique: true, name: 'index_boards_epic_user_preferences_on_board_user_epic_unique'
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :boards_epic_user_preferences
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BoardsEpicUserPreferencesFkBoard < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_foreign_key :boards_epic_user_preferences, :boards, column: :board_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :boards_epic_user_preferences, column: :board_id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BoardsEpicUserPreferencesFkUser < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_foreign_key :boards_epic_user_preferences, :users, column: :user_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :boards_epic_user_preferences, column: :user_id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BoardsEpicUserPreferencesFkEpic < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_foreign_key :boards_epic_user_preferences, :epics, column: :epic_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :boards_epic_user_preferences, column: :epic_id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexExpireAtToPipelineArtifacts < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
INDEX_NAME = 'index_ci_pipeline_artifacts_on_expire_at'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :ci_pipeline_artifacts, :expire_at, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(:ci_pipeline_artifacts, INDEX_NAME)
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
1ee7ae93dde7099f78cd6218b5419a34b2cfebe196521bcbee1583e31f19ffda
|
|
@ -0,0 +1 @@
|
|||
26fe286e565f776f64ae8b6b0ad91ef1d3bf2195384f44f8b093a1b66ee0d05d
|
|
@ -0,0 +1 @@
|
|||
deb88efebc989a014b6ecaca4a91624d1b21f34c85cbf6d3460363f1b498b427
|
|
@ -0,0 +1 @@
|
|||
8fc437f09321cfe29262075009bce6f7b0047c2291df4a29bcc304c6dd54d27d
|
|
@ -0,0 +1 @@
|
|||
85b7ffba53c9cec30e9778dd806277ca8e9877c9a18dc1d6004402c0e66b8ef1
|
|
@ -9672,6 +9672,23 @@ CREATE TABLE public.boards (
|
|||
hide_closed_list boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE public.boards_epic_user_preferences (
|
||||
id bigint NOT NULL,
|
||||
board_id bigint NOT NULL,
|
||||
user_id bigint NOT NULL,
|
||||
epic_id bigint NOT NULL,
|
||||
collapsed boolean DEFAULT false NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE public.boards_epic_user_preferences_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE public.boards_epic_user_preferences_id_seq OWNED BY public.boards_epic_user_preferences.id;
|
||||
|
||||
CREATE SEQUENCE public.boards_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
|
@ -16845,6 +16862,8 @@ ALTER TABLE ONLY public.board_user_preferences ALTER COLUMN id SET DEFAULT nextv
|
|||
|
||||
ALTER TABLE ONLY public.boards ALTER COLUMN id SET DEFAULT nextval('public.boards_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY public.boards_epic_user_preferences ALTER COLUMN id SET DEFAULT nextval('public.boards_epic_user_preferences_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY public.broadcast_messages ALTER COLUMN id SET DEFAULT nextval('public.broadcast_messages_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY public.chat_names ALTER COLUMN id SET DEFAULT nextval('public.chat_names_id_seq'::regclass);
|
||||
|
@ -17774,6 +17793,9 @@ ALTER TABLE ONLY public.board_project_recent_visits
|
|||
ALTER TABLE ONLY public.board_user_preferences
|
||||
ADD CONSTRAINT board_user_preferences_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.boards_epic_user_preferences
|
||||
ADD CONSTRAINT boards_epic_user_preferences_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY public.boards
|
||||
ADD CONSTRAINT boards_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -19219,6 +19241,14 @@ CREATE INDEX index_board_user_preferences_on_user_id ON public.board_user_prefer
|
|||
|
||||
CREATE UNIQUE INDEX index_board_user_preferences_on_user_id_and_board_id ON public.board_user_preferences USING btree (user_id, board_id);
|
||||
|
||||
CREATE INDEX index_boards_epic_user_preferences_on_board_id ON public.boards_epic_user_preferences USING btree (board_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_boards_epic_user_preferences_on_board_user_epic_unique ON public.boards_epic_user_preferences USING btree (board_id, user_id, epic_id);
|
||||
|
||||
CREATE INDEX index_boards_epic_user_preferences_on_epic_id ON public.boards_epic_user_preferences USING btree (epic_id);
|
||||
|
||||
CREATE INDEX index_boards_epic_user_preferences_on_user_id ON public.boards_epic_user_preferences USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_boards_on_group_id ON public.boards USING btree (group_id);
|
||||
|
||||
CREATE INDEX index_boards_on_milestone_id ON public.boards USING btree (milestone_id);
|
||||
|
@ -19329,6 +19359,8 @@ CREATE INDEX index_ci_job_variables_on_job_id ON public.ci_job_variables USING b
|
|||
|
||||
CREATE UNIQUE INDEX index_ci_job_variables_on_key_and_job_id ON public.ci_job_variables USING btree (key, job_id);
|
||||
|
||||
CREATE INDEX index_ci_pipeline_artifacts_on_expire_at ON public.ci_pipeline_artifacts USING btree (expire_at);
|
||||
|
||||
CREATE INDEX index_ci_pipeline_artifacts_on_pipeline_id ON public.ci_pipeline_artifacts USING btree (pipeline_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_ci_pipeline_artifacts_on_pipeline_id_and_file_type ON public.ci_pipeline_artifacts USING btree (pipeline_id, file_type);
|
||||
|
@ -22243,6 +22275,9 @@ ALTER TABLE ONLY public.group_custom_attributes
|
|||
ALTER TABLE ONLY public.cluster_agents
|
||||
ADD CONSTRAINT fk_rails_25e9fc2d5d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.boards_epic_user_preferences
|
||||
ADD CONSTRAINT fk_rails_268c57d62d FOREIGN KEY (board_id) REFERENCES public.boards(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.group_wiki_repositories
|
||||
ADD CONSTRAINT fk_rails_26f867598c FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -22678,6 +22713,9 @@ ALTER TABLE ONLY public.x509_certificates
|
|||
ALTER TABLE ONLY public.pages_domain_acme_orders
|
||||
ADD CONSTRAINT fk_rails_76581b1c16 FOREIGN KEY (pages_domain_id) REFERENCES public.pages_domains(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.boards_epic_user_preferences
|
||||
ADD CONSTRAINT fk_rails_76c4e9732d FOREIGN KEY (epic_id) REFERENCES public.epics(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.ci_subscriptions_projects
|
||||
ADD CONSTRAINT fk_rails_7871f9a97b FOREIGN KEY (upstream_project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -22711,6 +22749,9 @@ ALTER TABLE ONLY public.approval_merge_request_rules_users
|
|||
ALTER TABLE ONLY public.dast_site_profiles
|
||||
ADD CONSTRAINT fk_rails_83e309d69e FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.boards_epic_user_preferences
|
||||
ADD CONSTRAINT fk_rails_851fe1510a FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY public.deployment_merge_requests
|
||||
ADD CONSTRAINT fk_rails_86a6d8bf12 FOREIGN KEY (merge_request_id) REFERENCES public.merge_requests(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -2908,7 +2908,12 @@ type DastScannerProfile {
|
|||
"""
|
||||
ID of the DAST scanner profile
|
||||
"""
|
||||
id: ID!
|
||||
globalId: DastScannerProfileID!
|
||||
|
||||
"""
|
||||
ID of the DAST scanner profile. Deprecated in 13.4: Use `global_id`
|
||||
"""
|
||||
id: ID! @deprecated(reason: "Use `global_id`. Deprecated in 13.4")
|
||||
|
||||
"""
|
||||
Name of the DAST scanner profile
|
||||
|
@ -2993,7 +2998,12 @@ type DastScannerProfileCreatePayload {
|
|||
"""
|
||||
ID of the scanner profile.
|
||||
"""
|
||||
id: ID
|
||||
globalId: DastScannerProfileID
|
||||
|
||||
"""
|
||||
ID of the scanner profile.. Deprecated in 13.4: Use `global_id`
|
||||
"""
|
||||
id: ID @deprecated(reason: "Use `global_id`. Deprecated in 13.4")
|
||||
}
|
||||
|
||||
"""
|
||||
|
|
|
@ -7866,10 +7866,28 @@
|
|||
"description": "Represents a DAST scanner profile.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"name": "globalId",
|
||||
"description": "ID of the DAST scanner profile",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DastScannerProfileID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"description": "ID of the DAST scanner profile. Deprecated in 13.4: Use `global_id`",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
|
@ -7880,8 +7898,8 @@
|
|||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "Use `global_id`. Deprecated in 13.4"
|
||||
},
|
||||
{
|
||||
"name": "profileName",
|
||||
|
@ -8115,18 +8133,32 @@
|
|||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"name": "globalId",
|
||||
"description": "ID of the scanner profile.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DastScannerProfileID",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"description": "ID of the scanner profile.. Deprecated in 13.4: Use `global_id`",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "Use `global_id`. Deprecated in 13.4"
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
|
|
@ -506,7 +506,8 @@ Represents a DAST scanner profile.
|
|||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `id` | ID! | ID of the DAST scanner profile |
|
||||
| `globalId` | DastScannerProfileID! | ID of the DAST scanner profile |
|
||||
| `id` **{warning-solid}** | ID! | **Deprecated:** Use `global_id`. Deprecated in 13.4 |
|
||||
| `profileName` | String | Name of the DAST scanner profile |
|
||||
| `spiderTimeout` | Int | The maximum number of seconds allowed for the spider to traverse the site |
|
||||
| `targetTimeout` | Int | The maximum number of seconds allowed for the site under test to respond to a request |
|
||||
|
@ -519,7 +520,8 @@ Autogenerated return type of DastScannerProfileCreate
|
|||
| --- | ---- | ---------- |
|
||||
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
|
||||
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
|
||||
| `id` | ID | ID of the scanner profile. |
|
||||
| `globalId` | DastScannerProfileID | ID of the scanner profile. |
|
||||
| `id` **{warning-solid}** | ID | **Deprecated:** Use `global_id`. Deprecated in 13.4 |
|
||||
|
||||
## DastScannerProfileUpdatePayload
|
||||
|
||||
|
|
|
@ -1170,7 +1170,7 @@ DELETE /groups/:id/share/:group_id
|
|||
|
||||
## Push Rules **(STARTER)**
|
||||
|
||||
### Get group push rules
|
||||
### Get group push rules **(STARTER)**
|
||||
|
||||
Get the [push rules](../user/group/index.md#group-push-rules-starter) of a group.
|
||||
|
||||
|
@ -1233,3 +1233,70 @@ POST /groups/:id/push_rule
|
|||
| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) allowed |
|
||||
| `commit_committer_check` **(PREMIUM)** | boolean | no | Only commits pushed using verified emails will be allowed |
|
||||
| `reject_unsigned_commits` **(PREMIUM)** | boolean | no | Only commits signed through GPG will be allowed |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/19/push_rule"
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 19,
|
||||
"created_at": "2020-08-31T15:53:00.073Z",
|
||||
"commit_message_regex": "[a-zA-Z]",
|
||||
"commit_message_negative_regex": "[x+]",
|
||||
"branch_name_regex": null,
|
||||
"deny_delete_tag": false,
|
||||
"member_check": false,
|
||||
"prevent_secrets": false,
|
||||
"author_email_regex": "^[A-Za-z0-9.]+@gitlab.com$",
|
||||
"file_name_regex": null,
|
||||
"max_file_size": 100
|
||||
}
|
||||
```
|
||||
|
||||
### Edit group push rule **(STARTER)**
|
||||
|
||||
Edit push rules for a specified group.
|
||||
|
||||
```plaintext
|
||||
PUT /groups/:id/push_rule
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------------------------------------------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
|
||||
| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag |
|
||||
| `member_check` **(STARTER)** | boolean | no | Restricts commits to be authored by existing GitLab users only |
|
||||
| `prevent_secrets` **(STARTER)** | boolean | no | [Files that are likely to contain secrets](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/gitlab/checks/files_denylist.yml) will be rejected |
|
||||
| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match the regular expression provided in this attribute, e.g. `Fixed \d+\..*` |
|
||||
| `commit_message_negative_regex` **(STARTER)** | string | no | Commit messages matching the regular expression provided in this attribute will not be allowed, e.g. `ssh\:\/\/` |
|
||||
| `branch_name_regex` **(STARTER)** | string | no | All branch names must match the regular expression provided in this attribute, e.g. `(feature|hotfix)\/*` |
|
||||
| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match the regular expression provided in this attribute, e.g. `@my-company.com$` |
|
||||
| `file_name_regex` **(STARTER)** | string | no | Filenames matching the regular expression provided in this attribute will **not** be allowed, e.g. `(jar|exe)$` |
|
||||
| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) allowed |
|
||||
| `commit_committer_check` **(PREMIUM)** | boolean | no | Only commits pushed using verified emails will be allowed |
|
||||
| `reject_unsigned_commits` **(PREMIUM)** | boolean | no | Only commits signed through GPG will be allowed |
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/19/push_rule"
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 19,
|
||||
"created_at": "2020-08-31T15:53:00.073Z",
|
||||
"commit_message_regex": "[a-zA-Z]",
|
||||
"commit_message_negative_regex": "[x+]",
|
||||
"branch_name_regex": null,
|
||||
"deny_delete_tag": false,
|
||||
"member_check": false,
|
||||
"prevent_secrets": false,
|
||||
"author_email_regex": "^[A-Za-z0-9.]+@staging.gitlab.com$",
|
||||
"file_name_regex": null,
|
||||
"max_file_size": 100
|
||||
}
|
||||
```
|
||||
|
|
|
@ -4,9 +4,10 @@ group: Ecosystem
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
|
||||
---
|
||||
|
||||
# GitLab Jira Development Panel integration **(PREMIUM)**
|
||||
# GitLab Jira Development Panel integration **(CORE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2381) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.0.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2381) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.0.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/233149) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.4.
|
||||
|
||||
The Jira Development Panel integration allows you to reference Jira issues within GitLab, displaying activity in the [Development panel](https://support.atlassian.com/jira-software-cloud/docs/view-development-information-for-an-issue/) in the issue. It complements the [GitLab Jira integration](../user/project/integrations/jira.md). You may choose to configure both integrations to take advantage of both sets of features. (See a [feature comparison](../user/project/integrations/jira_integrations.md#feature-comparison)).
|
||||
|
||||
|
@ -199,9 +200,8 @@ Potential resolutions:
|
|||
|
||||
- If you're using GitLab versions 11.10-12.7, upgrade to GitLab 12.8.10 or later
|
||||
to resolve an identified [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/37012).
|
||||
- The Jira Development Panel integration requires GitLab Premium, GitLab.com Silver,
|
||||
or a higher tier. If you're using a lower tier of GitLab, you'll need to upgrade
|
||||
to use this feature.
|
||||
- If you're using GitLab Core or GitLab Starter, be sure you're using
|
||||
GitLab 13.4 or later.
|
||||
|
||||
[Contact GitLab Support](https://about.gitlab.com/support) if none of these reasons apply.
|
||||
|
||||
|
@ -234,7 +234,9 @@ For a walkthrough of the integration with GitLab for Jira, watch [Configure GitL
|
|||
1. After installing, click **Get started** to go to the configurations page. This page is always available under **Jira Settings > Apps > Manage apps**.
|
||||
|
||||
![Start GitLab App configuration on Jira](img/jira_dev_panel_setup_com_2.png)
|
||||
1. Enter the group or personal namespace in the **Namespace** field and click **Link namespace to Jira**. Make sure you are logged in on GitLab.com and the namespace has a Silver or above license. The user setting up _GitLab for Jira_ must have **Maintainer** access to the GitLab namespace.
|
||||
1. In **Namespace**, enter the group or personal namespace, and then click
|
||||
**Link namespace to Jira**. The user setting up *GitLab for Jira* must have
|
||||
*Maintainer* access to the GitLab namespace.
|
||||
|
||||
NOTE: **Note:**
|
||||
The GitLab user only needs access when adding a new namespace. For syncing with Jira, we do not depend on the user's token.
|
||||
|
|
|
@ -18,7 +18,7 @@ Although you can [migrate](../../../user/project/import/jira.md) your Jira issue
|
|||
The following Jira integrations allow different types of cross-referencing between GitLab activity and Jira issues, with additional features:
|
||||
|
||||
- [**Jira integration**](jira.md) - This is built in to GitLab. In a given GitLab project, it can be configured to connect to any Jira instance, self-managed or Cloud.
|
||||
- [**Jira development panel integration**](../../../integration/jira_development_panel.md) **(PREMIUM)** - This connects all GitLab projects under a specified group or personal namespace.
|
||||
- [**Jira development panel integration**](../../../integration/jira_development_panel.md) - This connects all GitLab projects under a specified group or personal namespace.
|
||||
- If you're using Jira Cloud and GitLab.com, install the [GitLab for Jira](https://marketplace.atlassian.com/apps/1221011/gitlab-for-jira) app in the Atlassian Marketplace and see its [documentation](../../../integration/jira_development_panel.md#gitlab-for-jira-app).
|
||||
- For all other environments, use the [Jira DVCS Connector configuration instructions](../../../integration/jira_development_panel.md#configuration).
|
||||
|
||||
|
|
|
@ -246,6 +246,16 @@ module API
|
|||
mount ::API::Internal::Pages
|
||||
mount ::API::Internal::Kubernetes
|
||||
|
||||
version 'v3', using: :path do
|
||||
# Although the following endpoints are kept behind V3 namespace,
|
||||
# they're not deprecated neither should be removed when V3 get
|
||||
# removed. They're needed as a layer to integrate with Jira
|
||||
# Development Panel.
|
||||
namespace '/', requirements: ::API::V3::Github::ENDPOINT_REQUIREMENTS do
|
||||
mount ::API::V3::Github
|
||||
end
|
||||
end
|
||||
|
||||
route :any, '*path' do
|
||||
error!('404 Not Found', 404)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,217 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Simplified version of Github API entities.
|
||||
# It's mainly used to mimic Github API and integrate with Jira Development Panel.
|
||||
#
|
||||
module API
|
||||
module Github
|
||||
module Entities
|
||||
class Repository < Grape::Entity
|
||||
expose :id
|
||||
expose :owner do |project, options|
|
||||
root_namespace = options[:root_namespace] || project.root_namespace
|
||||
|
||||
{ login: root_namespace.path }
|
||||
end
|
||||
expose :name do |project, options|
|
||||
::Gitlab::Jira::Dvcs.encode_project_name(project)
|
||||
end
|
||||
end
|
||||
|
||||
class BranchCommit < Grape::Entity
|
||||
expose :id, as: :sha
|
||||
expose :type do |_|
|
||||
'commit'
|
||||
end
|
||||
end
|
||||
|
||||
class RepoCommit < Grape::Entity
|
||||
expose :id, as: :sha
|
||||
expose :author do |commit|
|
||||
{
|
||||
login: commit.author&.username,
|
||||
email: commit.author_email
|
||||
}
|
||||
end
|
||||
expose :committer do |commit|
|
||||
{
|
||||
login: commit.author&.username,
|
||||
email: commit.committer_email
|
||||
}
|
||||
end
|
||||
expose :commit do |commit|
|
||||
{
|
||||
author: {
|
||||
name: commit.author_name,
|
||||
email: commit.author_email,
|
||||
date: commit.authored_date.iso8601,
|
||||
type: 'User'
|
||||
},
|
||||
committer: {
|
||||
name: commit.committer_name,
|
||||
email: commit.committer_email,
|
||||
date: commit.committed_date.iso8601,
|
||||
type: 'User'
|
||||
},
|
||||
message: commit.safe_message
|
||||
}
|
||||
end
|
||||
expose :parents do |commit|
|
||||
commit.parent_ids.map { |id| { sha: id } }
|
||||
end
|
||||
expose :files do |commit|
|
||||
commit.diffs.diff_files.flat_map do |diff|
|
||||
additions = diff.added_lines
|
||||
deletions = diff.removed_lines
|
||||
|
||||
if diff.new_file?
|
||||
{
|
||||
status: 'added',
|
||||
filename: diff.new_path,
|
||||
additions: additions,
|
||||
changes: additions
|
||||
}
|
||||
elsif diff.deleted_file?
|
||||
{
|
||||
status: 'removed',
|
||||
filename: diff.old_path,
|
||||
deletions: deletions,
|
||||
changes: deletions
|
||||
}
|
||||
elsif diff.renamed_file?
|
||||
[
|
||||
{
|
||||
status: 'removed',
|
||||
filename: diff.old_path,
|
||||
deletions: deletions,
|
||||
changes: deletions
|
||||
},
|
||||
{
|
||||
status: 'added',
|
||||
filename: diff.new_path,
|
||||
additions: additions,
|
||||
changes: additions
|
||||
}
|
||||
]
|
||||
else
|
||||
{
|
||||
status: 'modified',
|
||||
filename: diff.new_path,
|
||||
additions: additions,
|
||||
deletions: deletions,
|
||||
changes: (additions + deletions)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Branch < Grape::Entity
|
||||
expose :name
|
||||
|
||||
expose :commit, using: BranchCommit do |repo_branch, options|
|
||||
options[:project].repository.commit(repo_branch.dereferenced_target)
|
||||
end
|
||||
end
|
||||
|
||||
class User < Grape::Entity
|
||||
expose :id
|
||||
expose :username, as: :login
|
||||
expose :user_url, as: :url
|
||||
expose :user_url, as: :html_url
|
||||
expose :avatar_url
|
||||
|
||||
private
|
||||
|
||||
def user_url
|
||||
Gitlab::Routing.url_helpers.user_url(object)
|
||||
end
|
||||
end
|
||||
|
||||
class NoteableComment < Grape::Entity
|
||||
expose :id
|
||||
expose :author, as: :user, using: User
|
||||
expose :note, as: :body
|
||||
expose :created_at
|
||||
end
|
||||
|
||||
class PullRequest < Grape::Entity
|
||||
expose :title
|
||||
expose :assignee, using: User do |merge_request|
|
||||
merge_request.assignee
|
||||
end
|
||||
expose :author, as: :user, using: User
|
||||
expose :created_at
|
||||
expose :description, as: :body
|
||||
# Since Jira service requests `/repos/-/jira/pulls` (without project
|
||||
# scope), we need to make it work with ID instead IID.
|
||||
expose :id, as: :number
|
||||
# GitHub doesn't have a "merged" or "closed" state. It's just "open" or
|
||||
# "closed".
|
||||
expose :state do |merge_request|
|
||||
case merge_request.state
|
||||
when 'opened', 'locked'
|
||||
'open'
|
||||
when 'merged'
|
||||
'closed'
|
||||
else
|
||||
merge_request.state
|
||||
end
|
||||
end
|
||||
expose :merged?, as: :merged
|
||||
expose :merged_at do |merge_request|
|
||||
merge_request.metrics&.merged_at
|
||||
end
|
||||
expose :closed_at do |merge_request|
|
||||
merge_request.metrics&.latest_closed_at
|
||||
end
|
||||
expose :updated_at
|
||||
expose :html_url do |merge_request|
|
||||
Gitlab::UrlBuilder.build(merge_request)
|
||||
end
|
||||
expose :head do
|
||||
expose :source_branch, as: :label
|
||||
expose :source_branch, as: :ref
|
||||
expose :source_project, as: :repo, using: Repository
|
||||
end
|
||||
expose :base do
|
||||
expose :target_branch, as: :label
|
||||
expose :target_branch, as: :ref
|
||||
expose :target_project, as: :repo, using: Repository
|
||||
end
|
||||
end
|
||||
|
||||
class PullRequestPayload < Grape::Entity
|
||||
expose :action do |merge_request|
|
||||
case merge_request.state
|
||||
when 'merged', 'closed'
|
||||
'closed'
|
||||
else
|
||||
'opened'
|
||||
end
|
||||
end
|
||||
|
||||
expose :id
|
||||
expose :pull_request, using: PullRequest do |merge_request|
|
||||
merge_request
|
||||
end
|
||||
end
|
||||
|
||||
class PullRequestEvent < Grape::Entity
|
||||
expose :id do |merge_request|
|
||||
updated_at = merge_request.updated_at.to_i
|
||||
"#{merge_request.id}-#{updated_at}"
|
||||
end
|
||||
expose :type do |_merge_request|
|
||||
'PullRequestEvent'
|
||||
end
|
||||
expose :updated_at, as: :created_at
|
||||
expose :payload, using: PullRequestPayload do |merge_request|
|
||||
# The merge request data is used by PullRequestPayload and PullRequest, so we just provide it
|
||||
# here. Otherwise Grape::Entity would try to access a field "payload" on Merge Request.
|
||||
merge_request
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,232 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# These endpoints partially mimic Github API behavior in order to successfully
|
||||
# integrate with Jira Development Panel.
|
||||
# Endpoints returning an empty list were temporarily added to avoid 404's
|
||||
# during Jira's DVCS integration.
|
||||
#
|
||||
module API
|
||||
module V3
|
||||
class Github < Grape::API::Instance
|
||||
NO_SLASH_URL_PART_REGEX = %r{[^/]+}.freeze
|
||||
ENDPOINT_REQUIREMENTS = {
|
||||
namespace: NO_SLASH_URL_PART_REGEX,
|
||||
project: NO_SLASH_URL_PART_REGEX,
|
||||
username: NO_SLASH_URL_PART_REGEX
|
||||
}.freeze
|
||||
|
||||
# Used to differentiate Jira Cloud requests from Jira Server requests
|
||||
# Jira Cloud user agent format: Jira DVCS Connector Vertigo/version
|
||||
# Jira Server user agent format: Jira DVCS Connector/version
|
||||
JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'.freeze
|
||||
|
||||
include PaginationParams
|
||||
|
||||
before do
|
||||
authorize_jira_user_agent!(request)
|
||||
authenticate!
|
||||
end
|
||||
|
||||
helpers do
|
||||
params :project_full_path do
|
||||
requires :namespace, type: String
|
||||
requires :project, type: String
|
||||
end
|
||||
|
||||
def authorize_jira_user_agent!(request)
|
||||
not_found! unless Gitlab::Jira::Middleware.jira_dvcs_connector?(request.env)
|
||||
end
|
||||
|
||||
def update_project_feature_usage_for(project)
|
||||
# Prevent errors on GitLab Geo not allowing
|
||||
# UPDATE statements to happen in GET requests.
|
||||
return if Gitlab::Database.read_only?
|
||||
|
||||
project.log_jira_dvcs_integration_usage(cloud: jira_cloud?)
|
||||
end
|
||||
|
||||
def jira_cloud?
|
||||
request.env['HTTP_USER_AGENT'].include?(JIRA_DVCS_CLOUD_USER_AGENT)
|
||||
end
|
||||
|
||||
def find_project_with_access(params)
|
||||
project = find_project!(
|
||||
::Gitlab::Jira::Dvcs.restore_full_path(params.slice(:namespace, :project).symbolize_keys)
|
||||
)
|
||||
not_found! unless can?(current_user, :download_code, project)
|
||||
project
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def find_merge_requests
|
||||
merge_requests = authorized_merge_requests.reorder(updated_at: :desc)
|
||||
paginate(merge_requests)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def find_merge_request_with_access(id, access_level = :read_merge_request)
|
||||
merge_request = authorized_merge_requests.find_by(id: id)
|
||||
not_found! unless can?(current_user, access_level, merge_request)
|
||||
merge_request
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def authorized_merge_requests
|
||||
MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?).execute
|
||||
end
|
||||
|
||||
def authorized_merge_requests_for_project(project)
|
||||
MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?, project_id: project.id).execute
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def find_notes(noteable)
|
||||
# They're not presented on Jira Dev Panel ATM. A comments count with a
|
||||
# redirect link is presented.
|
||||
notes = paginate(noteable.notes.user.reorder(nil))
|
||||
notes.select { |n| n.readable_by?(current_user) }
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
end
|
||||
|
||||
resource :orgs do
|
||||
get ':namespace/repos' do
|
||||
present []
|
||||
end
|
||||
end
|
||||
|
||||
resource :user do
|
||||
get :repos do
|
||||
present []
|
||||
end
|
||||
end
|
||||
|
||||
resource :users do
|
||||
params do
|
||||
use :pagination
|
||||
end
|
||||
|
||||
get ':namespace/repos' do
|
||||
namespace = Namespace.find_by_full_path(params[:namespace])
|
||||
not_found!('Namespace') unless namespace
|
||||
|
||||
projects = current_user.can_read_all_resources? ? Project.all : current_user.authorized_projects
|
||||
projects = projects.in_namespace(namespace.self_and_descendants)
|
||||
|
||||
projects_cte = Project.wrap_with_cte(projects)
|
||||
.eager_load_namespace_and_owner
|
||||
.with_route
|
||||
|
||||
present paginate(projects_cte),
|
||||
with: ::API::Github::Entities::Repository,
|
||||
root_namespace: namespace.root_ancestor
|
||||
end
|
||||
|
||||
get ':username' do
|
||||
forbidden! unless can?(current_user, :read_users_list)
|
||||
user = UsersFinder.new(current_user, { username: params[:username] }).execute.first
|
||||
not_found! unless user
|
||||
present user, with: ::API::Github::Entities::User
|
||||
end
|
||||
end
|
||||
|
||||
# Jira dev panel integration weirdly requests for "/-/jira/pulls" instead
|
||||
# "/api/v3/repos/<namespace>/<project>/pulls". This forces us into
|
||||
# returning _all_ Merge Requests from authorized projects (user is a member),
|
||||
# instead just the authorized MRs from a project.
|
||||
# Jira handles the filtering, presenting just MRs mentioning the Jira
|
||||
# issue ID on the MR title / description.
|
||||
resource :repos do
|
||||
# Keeping for backwards compatibility with old Jira integration instructions
|
||||
# so that users that do not change it will not suddenly have a broken integration
|
||||
get '/-/jira/pulls' do
|
||||
present find_merge_requests, with: ::API::Github::Entities::PullRequest
|
||||
end
|
||||
|
||||
get '/-/jira/events' do
|
||||
present []
|
||||
end
|
||||
|
||||
params do
|
||||
use :project_full_path
|
||||
end
|
||||
get ':namespace/:project/pulls' do
|
||||
user_project = find_project_with_access(params)
|
||||
|
||||
merge_requests = authorized_merge_requests_for_project(user_project)
|
||||
|
||||
present paginate(merge_requests), with: ::API::Github::Entities::PullRequest
|
||||
end
|
||||
|
||||
params do
|
||||
use :project_full_path
|
||||
end
|
||||
get ':namespace/:project/pulls/:id' do
|
||||
merge_request = find_merge_request_with_access(params[:id])
|
||||
|
||||
present merge_request, with: ::API::Github::Entities::PullRequest
|
||||
end
|
||||
|
||||
# In Github, each Merge Request is automatically also an issue.
|
||||
# Therefore we return its comments here.
|
||||
# It'll present _just_ the comments counting with a link to GitLab on
|
||||
# Jira dev panel, not the actual note content.
|
||||
get ':namespace/:project/issues/:id/comments' do
|
||||
merge_request = find_merge_request_with_access(params[:id])
|
||||
|
||||
present find_notes(merge_request), with: ::API::Github::Entities::NoteableComment
|
||||
end
|
||||
|
||||
# This refer to "review" comments but Jira dev panel doesn't seem to
|
||||
# present it accordingly.
|
||||
get ':namespace/:project/pulls/:id/comments' do
|
||||
present []
|
||||
end
|
||||
|
||||
# Commits are not presented within "Pull Requests" modal on Jira dev
|
||||
# panel.
|
||||
get ':namespace/:project/pulls/:id/commits' do
|
||||
present []
|
||||
end
|
||||
|
||||
# Self-hosted Jira (tested on 7.11.1) requests this endpoint right
|
||||
# after fetching branches.
|
||||
get ':namespace/:project/events' do
|
||||
user_project = find_project_with_access(params)
|
||||
|
||||
merge_requests = authorized_merge_requests_for_project(user_project)
|
||||
|
||||
present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent
|
||||
end
|
||||
|
||||
params do
|
||||
use :project_full_path
|
||||
use :pagination
|
||||
end
|
||||
get ':namespace/:project/branches' do
|
||||
user_project = find_project_with_access(params)
|
||||
|
||||
update_project_feature_usage_for(user_project)
|
||||
|
||||
branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
|
||||
|
||||
present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project
|
||||
end
|
||||
|
||||
params do
|
||||
use :project_full_path
|
||||
end
|
||||
get ':namespace/:project/commits/:sha' do
|
||||
user_project = find_project_with_access(params)
|
||||
|
||||
commit = user_project.commit(params[:sha])
|
||||
|
||||
not_found! 'Commit' unless commit
|
||||
|
||||
present commit, with: ::API::Github::Entities::RepoCommit
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Atlassian
|
||||
module JiraConnect
|
||||
class << self
|
||||
def app_name
|
||||
"GitLab for Jira (#{gitlab_host})"
|
||||
end
|
||||
|
||||
def app_key
|
||||
"gitlab-jira-connect-#{gitlab_host}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def gitlab_host
|
||||
Gitlab.config.gitlab.host
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Atlassian
|
||||
module JiraConnect
|
||||
class Client < Gitlab::HTTP
|
||||
def initialize(base_uri, shared_secret)
|
||||
@base_uri = base_uri
|
||||
@shared_secret = shared_secret
|
||||
end
|
||||
|
||||
def store_dev_info(project:, commits: nil, branches: nil, merge_requests: nil)
|
||||
dev_info_json = {
|
||||
repositories: [
|
||||
Serializers::RepositoryEntity.represent(
|
||||
project,
|
||||
commits: commits,
|
||||
branches: branches,
|
||||
merge_requests: merge_requests
|
||||
)
|
||||
]
|
||||
}.to_json
|
||||
|
||||
uri = URI.join(@base_uri, '/rest/devinfo/0.10/bulk')
|
||||
|
||||
headers = {
|
||||
'Authorization' => "JWT #{jwt_token('POST', uri)}",
|
||||
'Content-Type' => 'application/json'
|
||||
}
|
||||
|
||||
self.class.post(uri, headers: headers, body: dev_info_json)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def jwt_token(http_method, uri)
|
||||
claims = Atlassian::Jwt.build_claims(
|
||||
Atlassian::JiraConnect.app_key,
|
||||
uri,
|
||||
http_method,
|
||||
@base_uri
|
||||
)
|
||||
|
||||
Atlassian::Jwt.encode(claims, @shared_secret)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Atlassian
|
||||
module JiraConnect
|
||||
module Serializers
|
||||
class AuthorEntity < Grape::Entity
|
||||
include Gitlab::Routing
|
||||
|
||||
expose :name
|
||||
expose :email
|
||||
|
||||
with_options(unless: -> (user) { user.is_a?(CommitEntity::CommitAuthor) }) do
|
||||
expose :username
|
||||
expose :url do |user|
|
||||
user_url(user)
|
||||
end
|
||||
expose :avatar do |user|
|
||||
user.avatar_url(only_path: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Atlassian
|
||||
module JiraConnect
|
||||
module Serializers
|
||||
class BaseEntity < Grape::Entity
|
||||
include Gitlab::Routing
|
||||
include GitlabRoutingHelper
|
||||
|
||||
format_with(:string) { |value| value.to_s }
|
||||
|
||||
expose :monotonic_time, as: :updateSequenceId
|
||||
|
||||
private
|
||||
|
||||
def monotonic_time
|
||||
Gitlab::Metrics::System.monotonic_time.to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Atlassian
|
||||
module JiraConnect
|
||||
module Serializers
|
||||
class BranchEntity < BaseEntity
|
||||
expose :id do |branch|
|
||||
Digest::SHA256.hexdigest(branch.name)
|
||||
end
|
||||
expose :issueKeys do |branch|
|
||||
JiraIssueKeyExtractor.new(branch.name).issue_keys
|
||||
end
|
||||
expose :name
|
||||
expose :lastCommit, using: JiraConnect::Serializers::CommitEntity do |branch, options|
|
||||
options[:project].commit(branch.dereferenced_target)
|
||||
end
|
||||
|
||||
expose :url do |branch, options|
|
||||
project_commits_url(options[:project], branch.name)
|
||||
end
|
||||
expose :createPullRequestUrl do |branch, options|
|
||||
project_new_merge_request_url(
|
||||
options[:project],
|
||||
merge_request: {
|
||||
source_branch: branch.name
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Atlassian
|
||||
module JiraConnect
|
||||
module Serializers
|
||||
class CommitEntity < BaseEntity
|
||||
CommitAuthor = Struct.new(:name, :email)
|
||||
|
||||
expose :id
|
||||
expose :issueKeys do |commit|
|
||||
JiraIssueKeyExtractor.new(commit.safe_message).issue_keys
|
||||
end
|
||||
expose :id, as: :hash
|
||||
expose :short_id, as: :displayId
|
||||
expose :safe_message, as: :message
|
||||
expose :flags do |commit|
|
||||
if commit.merge_commit?
|
||||
['MERGE_COMMIT']
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
expose :author, using: JiraConnect::Serializers::AuthorEntity
|
||||
expose :fileCount do |commit|
|
||||
commit.stats.total
|
||||
end
|
||||
expose :files do |commit, options|
|
||||
files = commit.diffs(max_files: 10).diff_files
|
||||
JiraConnect::Serializers::FileEntity.represent files, options.merge(commit: commit)
|
||||
end
|
||||
expose :created_at, as: :authorTimestamp
|
||||
|
||||
expose :url do |commit, options|
|
||||
project_commit_url(options[:project], commit.id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def author
|
||||
object.author || CommitAuthor.new(object.author_name, object.author_email)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Atlassian
|
||||
module JiraConnect
|
||||
module Serializers
|
||||
class FileEntity < Grape::Entity
|
||||
include Gitlab::Routing
|
||||
|
||||
expose :path do |file|
|
||||
file.deleted_file? ? file.old_path : file.new_path
|
||||
end
|
||||
expose :changeType do |file|
|
||||
if file.new_file?
|
||||
'ADDED'
|
||||
elsif file.deleted_file?
|
||||
'DELETED'
|
||||
elsif file.renamed_file?
|
||||
'MOVED'
|
||||
else
|
||||
'MODIFIED'
|
||||
end
|
||||
end
|
||||
expose :added_lines, as: :linesAdded
|
||||
expose :removed_lines, as: :linesRemoved
|
||||
|
||||
expose :url do |file, options|
|
||||
file_path = if file.deleted_file?
|
||||
File.join(options[:commit].parent_id, file.old_path)
|
||||
else
|
||||
File.join(options[:commit].id, file.new_path)
|
||||
end
|
||||
|
||||
project_blob_url(options[:project], file_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Atlassian
|
||||
module JiraConnect
|
||||
module Serializers
|
||||
class PullRequestEntity < BaseEntity
|
||||
STATUS_MAPPING = {
|
||||
'opened' => 'OPEN',
|
||||
'locked' => 'OPEN',
|
||||
'merged' => 'MERGED',
|
||||
'closed' => 'DECLINED'
|
||||
}.freeze
|
||||
|
||||
expose :id, format_with: :string
|
||||
expose :issueKeys do |mr|
|
||||
JiraIssueKeyExtractor.new(mr.title, mr.description).issue_keys
|
||||
end
|
||||
expose :displayId do |mr|
|
||||
mr.to_reference(full: true)
|
||||
end
|
||||
expose :title
|
||||
expose :author, using: JiraConnect::Serializers::AuthorEntity
|
||||
expose :user_notes_count, as: :commentCount
|
||||
expose :source_branch, as: :sourceBranch
|
||||
expose :target_branch, as: :destinationBranch
|
||||
expose :lastUpdate do |mr|
|
||||
mr.last_edited_at || mr.created_at
|
||||
end
|
||||
expose :status do |mr|
|
||||
STATUS_MAPPING[mr.state] || 'UNKNOWN'
|
||||
end
|
||||
|
||||
expose :sourceBranchUrl do |mr|
|
||||
project_commits_url(mr.project, mr.source_branch)
|
||||
end
|
||||
expose :url do |mr|
|
||||
merge_request_url(mr)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Atlassian
|
||||
module JiraConnect
|
||||
module Serializers
|
||||
class RepositoryEntity < BaseEntity
|
||||
expose :id, format_with: :string
|
||||
expose :name
|
||||
expose :description
|
||||
expose :url do |project|
|
||||
project_url(project)
|
||||
end
|
||||
expose :avatar do |project|
|
||||
project.avatar_url(only_path: false)
|
||||
end
|
||||
|
||||
expose :commits do |project, options|
|
||||
JiraConnect::Serializers::CommitEntity.represent options[:commits], project: project
|
||||
end
|
||||
expose :branches do |project, options|
|
||||
JiraConnect::Serializers::BranchEntity.represent options[:branches], project: project
|
||||
end
|
||||
expose :pullRequests do |project, options|
|
||||
JiraConnect::Serializers::PullRequestEntity.represent options[:merge_requests], project: project
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Atlassian
|
||||
class JiraIssueKeyExtractor
|
||||
def self.has_keys?(*text)
|
||||
new(*text).issue_keys.any?
|
||||
end
|
||||
|
||||
def initialize(*text)
|
||||
@text = text.join(' ')
|
||||
end
|
||||
|
||||
def issue_keys
|
||||
@text.scan(Gitlab::Regex.jira_issue_key_regex).uniq
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Constraints
|
||||
class JiraEncodedUrlConstrainer
|
||||
def matches?(request)
|
||||
request.path.starts_with?('/-/jira') || request.params[:project_id].include?(Gitlab::Jira::Dvcs::ENCODED_SLASH)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -137,6 +137,12 @@ class Feature
|
|||
Feature::Definition.load_all!
|
||||
end
|
||||
|
||||
def register_hot_reloader
|
||||
return unless check_feature_flags_definition?
|
||||
|
||||
Feature::Definition.register_hot_reloader!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def flipper
|
||||
|
|
|
@ -107,6 +107,20 @@ class Feature
|
|||
end
|
||||
end
|
||||
|
||||
def register_hot_reloader!
|
||||
# Reload feature flags on change of this file or any `.yml`
|
||||
file_watcher = Rails.configuration.file_watcher.new(reload_files, reload_directories) do
|
||||
# We use `Feature::Definition` as on Ruby code-reload
|
||||
# a new class definition is created
|
||||
Feature::Definition.load_all!
|
||||
end
|
||||
|
||||
Rails.application.reloaders << file_watcher
|
||||
Rails.application.reloader.to_run { file_watcher.execute_if_updated }
|
||||
|
||||
file_watcher
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_from_file(path)
|
||||
|
@ -130,6 +144,19 @@ class Feature
|
|||
definitions[definition.key] = definition
|
||||
end
|
||||
end
|
||||
|
||||
def reload_files
|
||||
[File.expand_path(__FILE__)]
|
||||
end
|
||||
|
||||
def reload_directories
|
||||
paths.each_with_object({}) do |path, result|
|
||||
path = File.dirname(path)
|
||||
Dir.glob(path).each do |matching_dir|
|
||||
result[matching_dir] = 'yml'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -103,8 +103,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
# Private projects are not allowed to have enabled access level, only `private` and `public`
|
||||
# If access control is enabled, these projects currently behave as if the have `private` pages_access_level
|
||||
# if access control is disabled, these projects currently behave as if the have `public` pages_access_level
|
||||
# If access control is enabled, these projects currently behave as if they have `private` pages_access_level
|
||||
# if access control is disabled, these projects currently behave as if they have `public` pages_access_level
|
||||
# so we preserve this behaviour for projects with pages already deployed
|
||||
# for project without pages we always set `private` access_level
|
||||
def fix_private_access_level(start_id, stop_id)
|
||||
|
|
|
@ -25,7 +25,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def key_text
|
||||
if @key_text && @key_text.size <= MAX_KEY_SIZE
|
||||
if @key_text && @key_text.size <= MAX_KEY_TEXT_SIZE
|
||||
@key_text
|
||||
else
|
||||
@entity.to_s
|
||||
|
@ -37,7 +37,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def key_width
|
||||
if @key_width && @key_width.between?(1, MAX_KEY_SIZE)
|
||||
if @key_width && @key_width.between?(1, MAX_KEY_WIDTH)
|
||||
@key_width
|
||||
else
|
||||
62
|
||||
|
|
|
@ -29,7 +29,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def key_text
|
||||
if @key_text && @key_text.size <= MAX_KEY_SIZE
|
||||
if @key_text && @key_text.size <= MAX_KEY_TEXT_SIZE
|
||||
@key_text
|
||||
else
|
||||
@entity.to_s
|
||||
|
@ -41,7 +41,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def key_width
|
||||
if @key_width && @key_width.between?(1, MAX_KEY_SIZE)
|
||||
if @key_width && @key_width.between?(1, MAX_KEY_WIDTH)
|
||||
@key_width
|
||||
else
|
||||
62
|
||||
|
|
|
@ -6,7 +6,8 @@ module Gitlab
|
|||
# Abstract template class for badges
|
||||
#
|
||||
class Template
|
||||
MAX_KEY_SIZE = 128
|
||||
MAX_KEY_TEXT_SIZE = 64
|
||||
MAX_KEY_WIDTH = 512
|
||||
|
||||
def initialize(badge)
|
||||
@entity = badge.entity
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Jira
|
||||
module Dvcs
|
||||
ENCODED_SLASH = '@'.freeze
|
||||
SLASH = '/'.freeze
|
||||
ENCODED_ROUTE_REGEX = /[a-zA-Z0-9_\-\.#{ENCODED_SLASH}]+/.freeze
|
||||
|
||||
def self.encode_slash(path)
|
||||
path.gsub(SLASH, ENCODED_SLASH)
|
||||
end
|
||||
|
||||
def self.decode_slash(path)
|
||||
path.gsub(ENCODED_SLASH, SLASH)
|
||||
end
|
||||
|
||||
# To present two types of projects stored by Jira,
|
||||
# Type 1 are projects imported prior to nested group support,
|
||||
# those project names are not full_path, so they are presented differently
|
||||
# to maintain backwards compatibility.
|
||||
# Type 2 are projects imported after nested group support,
|
||||
# those project names are encoded full path
|
||||
#
|
||||
# @param [Project] project
|
||||
def self.encode_project_name(project)
|
||||
if project.namespace.has_parent?
|
||||
encode_slash(project.full_path)
|
||||
else
|
||||
project.path
|
||||
end
|
||||
end
|
||||
|
||||
# To interpret two types of project names stored by Jira (see `encode_project_name`)
|
||||
#
|
||||
# @param [String] project
|
||||
# Either an encoded full path, or just project name
|
||||
# @param [String] namespace
|
||||
def self.restore_full_path(namespace:, project:)
|
||||
if project.include?(ENCODED_SLASH)
|
||||
project.gsub(ENCODED_SLASH, SLASH)
|
||||
else
|
||||
"#{namespace}/#{project}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Jira
|
||||
class Middleware
|
||||
def self.jira_dvcs_connector?(env)
|
||||
env['HTTP_USER_AGENT']&.downcase&.start_with?('jira dvcs connector')
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if self.class.jira_dvcs_connector?(env)
|
||||
env['HTTP_AUTHORIZATION'] = env['HTTP_AUTHORIZATION']&.sub('token', 'Bearer')
|
||||
end
|
||||
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,7 +30,7 @@ module Gitlab
|
|||
set_cookie = headers['Set-Cookie']&.strip
|
||||
|
||||
return result if set_cookie.blank? || !ssl?
|
||||
return result if same_site_none_incompatible?(headers['User-Agent'])
|
||||
return result if same_site_none_incompatible?(env['HTTP_USER_AGENT'])
|
||||
|
||||
cookies = set_cookie.split(COOKIE_SEPARATOR)
|
||||
|
||||
|
|
|
@ -376,7 +376,9 @@ module Gitlab
|
|||
# so we can just check for subdomains of atlassian.net
|
||||
results = {
|
||||
projects_jira_server_active: 0,
|
||||
projects_jira_cloud_active: 0
|
||||
projects_jira_cloud_active: 0,
|
||||
projects_jira_dvcs_cloud_active: count(ProjectFeatureUsage.with_jira_dvcs_integration_enabled),
|
||||
projects_jira_dvcs_server_active: count(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false))
|
||||
}
|
||||
|
||||
# rubocop: disable UsageData/LargeTable:
|
||||
|
@ -566,7 +568,10 @@ module Gitlab
|
|||
projects: distinct_count(::Project.where(time_period), :creator_id),
|
||||
todos: distinct_count(::Todo.where(time_period), :author_id),
|
||||
service_desk_enabled_projects: distinct_count_service_desk_enabled_projects(time_period),
|
||||
service_desk_issues: count(::Issue.service_desk.where(time_period))
|
||||
service_desk_issues: count(::Issue.service_desk.where(time_period)),
|
||||
projects_jira_active: distinct_count(::Project.with_active_jira_services.where(time_period), :creator_id),
|
||||
projects_jira_dvcs_cloud_active: distinct_count(::Project.with_active_jira_services.with_jira_dvcs_cloud.where(time_period), :creator_id),
|
||||
projects_jira_dvcs_server_active: distinct_count(::Project.with_active_jira_services.with_jira_dvcs_server.where(time_period), :creator_id)
|
||||
}
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -13279,6 +13279,9 @@ msgstr ""
|
|||
msgid "Integrations|Comment settings:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Default settings are inherited from the group level."
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Default settings are inherited from the instance level."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"@babel/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.5",
|
||||
"@gitlab/svgs": "1.161.0",
|
||||
"@gitlab/ui": "20.12.1",
|
||||
"@gitlab/ui": "20.13.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.3-1",
|
||||
"@sentry/browser": "^5.10.2",
|
||||
|
|
|
@ -37,7 +37,7 @@ module RuboCop
|
|||
table, _, type = matched.to_a.take(3).map(&:children).map(&:first)
|
||||
opts = matched[3]
|
||||
|
||||
return unless WHITELISTED_TABLES.include?(table) && type == :boolean
|
||||
return unless SMALL_TABLES.include?(table) && type == :boolean
|
||||
|
||||
no_default = no_default?(opts)
|
||||
nulls_allowed = nulls_allowed?(opts)
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
module RuboCop
|
||||
# Module containing helper methods for writing migration cops.
|
||||
module MigrationHelpers
|
||||
WHITELISTED_TABLES = %i[
|
||||
# Tables with permanently small number of records
|
||||
SMALL_TABLES = %i[
|
||||
application_settings
|
||||
plan_limits
|
||||
].freeze
|
||||
|
||||
# Blacklisted tables due to:
|
||||
# - number of columns (> 50 on GitLab.com as of 03/2020)
|
||||
# - number of records
|
||||
# Tables with large number of columns (> 50 on GitLab.com as of 03/2020)
|
||||
WIDE_TABLES = %i[
|
||||
users
|
||||
projects
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe JiraConnect::AppDescriptorController do
|
||||
describe '#show' do
|
||||
it 'returns JSON app descriptor' do
|
||||
get :show
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response).to include(
|
||||
'baseUrl' => 'https://test.host/-/jira_connect',
|
||||
'lifecycle' => {
|
||||
'installed' => '/events/installed',
|
||||
'uninstalled' => '/events/uninstalled'
|
||||
},
|
||||
'links' => {
|
||||
'documentation' => 'http://test.host/help/integration/jira_development_panel#gitlabcom-1'
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,73 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe JiraConnect::EventsController do
|
||||
describe '#installed' do
|
||||
subject do
|
||||
post :installed, params: {
|
||||
clientKey: '1234',
|
||||
sharedSecret: 'secret',
|
||||
baseUrl: 'https://test.atlassian.net'
|
||||
}
|
||||
end
|
||||
|
||||
it 'saves the jira installation data' do
|
||||
expect { subject }.to change { JiraConnectInstallation.count }.by(1)
|
||||
end
|
||||
|
||||
it 'saves the correct values' do
|
||||
subject
|
||||
|
||||
installation = JiraConnectInstallation.find_by_client_key('1234')
|
||||
|
||||
expect(installation.shared_secret).to eq('secret')
|
||||
expect(installation.base_url).to eq('https://test.atlassian.net')
|
||||
end
|
||||
|
||||
context 'client key already exists' do
|
||||
it 'returns 422' do
|
||||
create(:jira_connect_installation, client_key: '1234')
|
||||
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#uninstalled' do
|
||||
let!(:installation) { create(:jira_connect_installation) }
|
||||
let(:qsh) { Atlassian::Jwt.create_query_string_hash('https://gitlab.test/events/uninstalled', 'POST', 'https://gitlab.test') }
|
||||
|
||||
before do
|
||||
request.headers['Authorization'] = "JWT #{auth_token}"
|
||||
end
|
||||
|
||||
subject { post :uninstalled }
|
||||
|
||||
context 'when JWT is invalid' do
|
||||
let(:auth_token) { 'invalid_token' }
|
||||
|
||||
it 'returns 403' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
||||
it 'does not delete the installation' do
|
||||
expect { subject }.not_to change { JiraConnectInstallation.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when JWT is valid' do
|
||||
let(:auth_token) do
|
||||
Atlassian::Jwt.encode({ iss: installation.client_key, qsh: qsh }, installation.shared_secret)
|
||||
end
|
||||
|
||||
it 'deletes the installation' do
|
||||
expect { subject }.to change { JiraConnectInstallation.count }.by(-1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue