summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.codeclimate.yml1
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.rubocop.yml1
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js4
-rw-r--r--app/assets/javascripts/environments/index.js (renamed from app/assets/javascripts/environments/environments_bundle.js)4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js10
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js27
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js20
-rw-r--r--app/assets/javascripts/pages/projects/environments/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js14
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js12
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js33
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js30
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/builds/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js6
-rw-r--r--app/assets/javascripts/pages/search/init_filtered_search.js16
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js4
-rw-r--r--app/assets/javascripts/protected_tags/index.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue10
-rw-r--r--app/services/system_hooks_service.rb2
-rw-r--r--app/views/groups/merge_requests.html.haml3
-rw-r--r--app/views/projects/branches/new.html.haml1
-rw-r--r--app/views/projects/commit/_pipelines_list.haml3
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/edit.html.haml1
-rw-r--r--app/views/projects/environments/folder.html.haml3
-rw-r--r--app/views/projects/environments/index.html.haml1
-rw-r--r--app/views/projects/environments/metrics.html.haml2
-rw-r--r--app/views/projects/graphs/charts.html.haml1
-rw-r--r--app/views/projects/issues/show.html.haml1
-rw-r--r--app/views/projects/pipelines/charts/_pipeline_times.haml1
-rw-r--r--app/views/projects/pipelines/charts/_pipelines.haml1
-rw-r--r--app/views/projects/pipelines/new.html.haml1
-rw-r--r--app/views/projects/pipelines/show.html.haml1
-rw-r--r--app/views/projects/protected_tags/_index.html.haml3
-rw-r--r--app/views/projects/tags/new.html.haml1
-rw-r--r--app/views/shared/boards/_show.html.haml1
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml5
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml1
-rw-r--r--app/views/u2f/_authenticate.html.haml1
-rw-r--r--app/views/u2f/_register.html.haml1
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/plugin_worker.rb15
-rw-r--r--changelogs/unreleased/dz-system-hooks-plugins.yml5
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--config/webpack.config.js61
-rw-r--r--doc/administration/plugins.md66
-rw-r--r--lib/gitlab/plugin.rb26
-rw-r--r--lib/gitlab/plugin_logger.rb7
-rw-r--r--lib/haml_lint/inline_javascript.rb6
-rw-r--r--lib/tasks/plugins.rake16
-rwxr-xr-xplugins/examples/save_to_file.clj3
-rwxr-xr-xplugins/examples/save_to_file.rb3
-rw-r--r--spec/javascripts/test_bundle.js1
-rw-r--r--spec/lib/gitlab/plugin_spec.rb68
-rw-r--r--spec/migrations/migrate_stages_statuses_spec.rb2
-rw-r--r--spec/migrations/schedule_build_stage_migration_spec.rb2
-rw-r--r--spec/requests/api/runner_spec.rb2
-rw-r--r--spec/support/gitlab-git-test.git/packed-refs2
-rw-r--r--spec/workers/plugin_worker_spec.rb25
64 files changed, 431 insertions, 141 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 333a70f71d3..216ecf43beb 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -45,3 +45,4 @@ exclude_paths:
- log/
- backups/
- coverage-javascript/
+- plugins/
diff --git a/.gitignore b/.gitignore
index 2004c2a09b4..fa39ae01ff0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,3 +66,4 @@ eslint-report.html
/locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec
+/plugins/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ae762e7aa6e..8a0c9802c15 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
diff --git a/.rubocop.yml b/.rubocop.yml
index 24edb641657..293f61fb725 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -17,6 +17,7 @@ AllCops:
- 'bin/**/*'
- 'generator_templates/**/*'
- 'builds/**/*'
+ - 'plugins/**/*'
CacheRootDirectory: tmp
# This cop checks whether some constant value isn't a
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 0df1f7a6f82..57a7cc4ca30 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -4,7 +4,9 @@ import FilteredSearchManager from '../filtered_search/filtered_search_manager';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
- super('boards');
+ super({
+ page: 'boards',
+ });
this.store = store;
this.updateUrl = updateUrl;
diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/index.js
index 2e0a4001b7c..afc4aba6554 100644
--- a/app/assets/javascripts/environments/environments_bundle.js
+++ b/app/assets/javascripts/environments/index.js
@@ -5,7 +5,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => new Vue({
+export default () => new Vue({
el: '#environments-list-view',
components: {
environmentsComponent,
@@ -36,4 +36,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
-}));
+});
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
deleted file mode 100644
index 293154917fa..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import './dropdown_emoji';
-import './dropdown_hint';
-import './dropdown_non_user';
-import './dropdown_user';
-import './dropdown_utils';
-import './filtered_search_dropdown_manager';
-import './filtered_search_dropdown';
-import './filtered_search_manager';
-import './filtered_search_tokenizer';
-import './filtered_search_visual_tokens';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index c64553a1b92..ee49a7be0b2 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -10,13 +10,22 @@ import DropdownUser from './dropdown_user';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager {
- constructor(baseEndpoint = '', tokenizer, page, isGroup, filteredSearchTokenKeys) {
+ constructor({
+ baseEndpoint = '',
+ tokenizer,
+ page,
+ isGroup,
+ isGroupAncestor,
+ filteredSearchTokenKeys,
+ }) {
this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page;
+ this.groupsOnly = isGroup;
+ this.groupAncestor = isGroupAncestor;
this.setupMapping();
@@ -59,7 +68,7 @@ export default class FilteredSearchDropdownManager {
reference: null,
gl: DropdownNonUser,
extraArguments: {
- endpoint: `${this.baseEndpoint}/milestones.json`,
+ endpoint: this.getMilestoneEndpoint(),
symbol: '%',
},
element: this.container.querySelector('#js-dropdown-milestone'),
@@ -68,7 +77,7 @@ export default class FilteredSearchDropdownManager {
reference: null,
gl: DropdownNonUser,
extraArguments: {
- endpoint: `${this.baseEndpoint}/labels.json`,
+ endpoint: this.getLabelsEndpoint(),
symbol: '~',
preprocessing: DropdownUtils.duplicateLabelPreprocessing,
},
@@ -90,6 +99,18 @@ export default class FilteredSearchDropdownManager {
this.mapping = allowedMappings;
}
+ getMilestoneEndpoint() {
+ const endpoint = `${this.baseEndpoint}/milestones.json`;
+
+ return endpoint;
+ }
+
+ getLabelsEndpoint() {
+ const endpoint = `${this.baseEndpoint}/labels.json`;
+
+ return endpoint;
+ }
+
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index e294b629bd0..c6970d7837f 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -20,10 +20,13 @@ import DropdownUtils from './dropdown_utils';
export default class FilteredSearchManager {
constructor({
page,
+ isGroup = false,
+ isGroupAncestor = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
}) {
- this.isGroup = false;
+ this.isGroup = isGroup;
+ this.isGroupAncestor = isGroupAncestor;
this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page;
@@ -75,13 +78,14 @@ export default class FilteredSearchManager {
if (this.filteredSearchInput) {
this.tokenizer = FilteredSearchTokenizer;
- this.dropdownManager = new FilteredSearchDropdownManager(
- this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
- this.tokenizer,
- this.page,
- this.isGroup,
- this.filteredSearchTokenKeys,
- );
+ this.dropdownManager = new FilteredSearchDropdownManager({
+ baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
+ tokenizer: this.tokenizer,
+ page: this.page,
+ isGroup: this.isGroup,
+ isGroupAncestor: this.isGroupAncestor,
+ filteredSearchTokenKeys: this.filteredSearchTokenKeys,
+ });
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
diff --git a/app/assets/javascripts/pages/projects/environments/index.js b/app/assets/javascripts/pages/projects/environments/index.js
new file mode 100644
index 00000000000..ace8af00ece
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/index.js
@@ -0,0 +1,3 @@
+import initEnviroments from '~/environments/';
+
+document.addEventListener('DOMContentLoaded', initEnviroments);
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
new file mode 100644
index 00000000000..37503cc1542
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -0,0 +1,14 @@
+import initIssuableSidebar from '~/init_issuable_sidebar';
+import Issue from '~/issue';
+import ShortcutsIssuable from '~/shortcuts_issuable';
+import ZenMode from '~/zen_mode';
+import '~/notes/index';
+import '~/issue_show/index';
+
+export default function () {
+ new Issue(); // eslint-disable-line no-new
+ new ShortcutsIssuable(); // eslint-disable-line no-new
+ new ZenMode(); // eslint-disable-line no-new
+ initIssuableSidebar();
+}
+
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index 3950fe0a1fd..7968dfd7a12 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,15 +1,7 @@
-import initIssuableSidebar from '~/init_issuable_sidebar';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import Issue from '~/issue';
-import ShortcutsIssuable from '~/shortcuts_issuable';
-import ZenMode from '~/zen_mode';
-import '~/notes/index';
-import '~/issue_show/index';
+import initShow from '../show';
document.addEventListener('DOMContentLoaded', () => {
- new Issue(); // eslint-disable-line no-new
- new ShortcutsIssuable(); // eslint-disable-line no-new
- new ZenMode(); // eslint-disable-line no-new
- initIssuableSidebar();
+ initShow();
initSidebarBundle();
});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
new file mode 100644
index 00000000000..da27c22f537
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -0,0 +1,33 @@
+import MergeRequest from '~/merge_request';
+import ZenMode from '~/zen_mode';
+import initNotes from '~/init_notes';
+import initIssuableSidebar from '~/init_issuable_sidebar';
+import initDiffNotes from '~/diff_notes/diff_notes_bundle';
+import ShortcutsIssuable from '~/shortcuts_issuable';
+import Diff from '~/diff';
+import { handleLocationHash } from '~/lib/utils/common_utils';
+import howToMerge from '~/how_to_merge';
+import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import initWidget from '../../../vue_merge_request_widget';
+
+export default function () {
+ new Diff(); // eslint-disable-line no-new
+ new ZenMode(); // eslint-disable-line no-new
+
+ initIssuableSidebar();
+ initNotes();
+ initDiffNotes();
+ initPipelines();
+
+ const mrShowNode = document.querySelector('.merge-request');
+
+ window.mergeRequest = new MergeRequest({
+ action: mrShowNode.dataset.mrAction,
+ });
+
+ new ShortcutsIssuable(true); // eslint-disable-line no-new
+ handleLocationHash();
+ howToMerge();
+ initWidget();
+}
+
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index b48ba1ef95e..3e72f7a6f37 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,33 +1,7 @@
-import MergeRequest from '~/merge_request';
-import ZenMode from '~/zen_mode';
-import initNotes from '~/init_notes';
-import initIssuableSidebar from '~/init_issuable_sidebar';
-import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import ShortcutsIssuable from '~/shortcuts_issuable';
-import Diff from '~/diff';
-import { handleLocationHash } from '~/lib/utils/common_utils';
-import howToMerge from '~/how_to_merge';
-import initPipelines from '~/commit/pipelines/pipelines_bundle';
-import initWidget from '../../../../vue_merge_request_widget';
+import initShow from '../init_merge_request_show';
document.addEventListener('DOMContentLoaded', () => {
- new Diff(); // eslint-disable-line no-new
- new ZenMode(); // eslint-disable-line no-new
-
- initIssuableSidebar();
+ initShow();
initSidebarBundle();
- initNotes();
- initDiffNotes();
- initPipelines();
-
- const mrShowNode = document.querySelector('.merge-request');
- window.mergeRequest = new MergeRequest({
- action: mrShowNode.dataset.mrAction,
- });
-
- new ShortcutsIssuable(true); // eslint-disable-line no-new
- handleLocationHash();
- howToMerge();
- initWidget();
});
diff --git a/app/assets/javascripts/pages/projects/pipelines/builds/index.js b/app/assets/javascripts/pages/projects/pipelines/builds/index.js
index fbe9824c34b..7a57e417b41 100644
--- a/app/assets/javascripts/pages/projects/pipelines/builds/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/builds/index.js
@@ -1,3 +1,7 @@
+import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines';
-document.addEventListener('DOMContentLoaded', initPipelines);
+document.addEventListener('DOMContentLoaded', () => {
+ initPipelines();
+ initPipelineDetails();
+});
diff --git a/app/assets/javascripts/pages/projects/pipelines/show/index.js b/app/assets/javascripts/pages/projects/pipelines/show/index.js
index fbe9824c34b..7a57e417b41 100644
--- a/app/assets/javascripts/pages/projects/pipelines/show/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/show/index.js
@@ -1,3 +1,7 @@
+import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines';
-document.addEventListener('DOMContentLoaded', initPipelines);
+document.addEventListener('DOMContentLoaded', () => {
+ initPipelines();
+ initPipelineDetails();
+});
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index 5a6f4138b10..001128ead59 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,7 +1,13 @@
+/* eslint-disable no-new */
+
+import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
+import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels';
import initDeployKeys from '~/deploy_keys';
document.addEventListener('DOMContentLoaded', () => {
+ new ProtectedTagCreate();
+ new ProtectedTagEditList();
initDeployKeys();
initSettingsPanels();
});
diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js
index de8d4168d71..57f08701a4f 100644
--- a/app/assets/javascripts/pages/search/init_filtered_search.js
+++ b/app/assets/javascripts/pages/search/init_filtered_search.js
@@ -1,9 +1,21 @@
import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
-export default ({ page }) => {
+export default ({
+ page,
+ filteredSearchTokenKeys,
+ isGroup,
+ isGroupAncestor,
+ stateFiltersSelector,
+}) => {
const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search');
if (filteredSearchEnabled) {
- const filteredSearchManager = new FilteredSearchManager({ page });
+ const filteredSearchManager = new FilteredSearchManager({
+ page,
+ isGroup,
+ isGroupAncestor,
+ filteredSearchTokenKeys,
+ stateFiltersSelector,
+ });
filteredSearchManager.setup();
}
};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 705a60b3ba2..6b26708148c 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -9,7 +9,7 @@ import eventHub from './event_hub';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
@@ -70,4 +70,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
-});
+};
diff --git a/app/assets/javascripts/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js
deleted file mode 100644
index b1618e24e49..00000000000
--- a/app/assets/javascripts/protected_tags/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-import ProtectedTagCreate from './protected_tag_create';
-import ProtectedTagEditList from './protected_tag_edit_list';
-
-$(() => {
- const protectedtTagCreate = new ProtectedTagCreate();
- const protectedtTagEditList = new ProtectedTagEditList();
-});
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index 3595a9389e9..c943c8d98a4 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -39,7 +39,7 @@
@click="onClick">
...
</button>
- <span v-show="!isCollapsed">
+ <span v-if="!isCollapsed">
<slot name="expanded"></slot>
</span>
</span>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 1413dd69f24..3fcacd156c5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -14,6 +14,11 @@
collapsedCalendarIcon,
},
props: {
+ blockClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
collapsed: {
type: Boolean,
required: false,
@@ -91,7 +96,10 @@
</script>
<template>
- <div class="block">
+ <div
+ class="block"
+ :class="blockClass"
+ >
<div class="issuable-sidebar-header">
<toggle-sidebar
:collapsed="collapsed"
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index a6b7a6e1416..af8c02a10b7 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -11,6 +11,8 @@ class SystemHooksService
SystemHook.hooks_for(hooks_scope).find_each do |hook|
hook.async_execute(data, 'system_hooks')
end
+
+ Gitlab::Plugin.execute_all_async(data)
end
private
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 520fe217ae1..4ccd16f3e11 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,8 +1,5 @@
- page_title "Merge Requests"
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
-
- if group_merge_requests_count(state: 'all').zero?
= render 'shared/empty_states/merge_requests', project_select_button: true
- else
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index e9d8fc75142..c7fc5a98ca8 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -28,4 +28,5 @@
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index a3fed25af28..68b35072f26 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -6,6 +6,3 @@
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
} }
-
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index ba86d943967..abb292f8f27 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -6,8 +6,6 @@
- @content_class = limited_container_width
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index b947b91322d..a96485ab155 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -70,6 +70,7 @@
Enable or disable certain project features and choose access levels.
.settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
+ -# haml-lint:disable InlineJavaScript
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index d8054dbc372..1ac7dab6775 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -1,9 +1,6 @@
- @no_container = true
- page_title "Environments"
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
-
#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
"folder-name" => @folder,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 31cf173fa9c..0d656b25bc8 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -4,7 +4,6 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag("common_vue")
- = webpack_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index 91b3743e9e7..9d9759ebc5f 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -1,7 +1,5 @@
- @no_container = true
- page_title "Metrics for environment", @environment.name
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
.prometheus-container{ class: container_class }
.top-area
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index d4b4a6203f3..14c47a5d91c 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -74,6 +74,7 @@
= _("Commits per day hour (UTC)")
%canvas#hour-chart
+-# haml-lint:disable InlineJavaScript
%script#projectChartData{ type: "application/json" }
- projectChartData = {};
- projectChartData['hour'] = @commits_per_time
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 77e6cb4d2cd..ec7e87219f5 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -55,6 +55,7 @@
.issue-details.issuable-details
.detail-page-description.content-block
+ -# haml-lint:disable InlineJavaScript
%script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json
#js-issuable-app
%h2.title= markdown_field(@issue, :title)
diff --git a/app/views/projects/pipelines/charts/_pipeline_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml
index 510697c2ae9..c23fe6ff170 100644
--- a/app/views/projects/pipelines/charts/_pipeline_times.haml
+++ b/app/views/projects/pipelines/charts/_pipeline_times.haml
@@ -4,4 +4,5 @@
%canvas#build_timesChart{ height: 200 }
+-# haml-lint:disable InlineJavaScript
%script#pipelinesTimesChartsData{ type: "application/json" }= { :labels => @charts[:pipeline_times].labels, :values => @charts[:pipeline_times].pipeline_times }.to_json.html_safe
diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml
index 2f4b6def155..14b3d47a9c2 100644
--- a/app/views/projects/pipelines/charts/_pipelines.haml
+++ b/app/views/projects/pipelines/charts/_pipelines.haml
@@ -26,6 +26,7 @@
= _("Pipelines for last year")
%canvas#yearChart.padded{ height: 250 }
+-# haml-lint:disable InlineJavaScript
%script#pipelinesChartsData{ type: "application/json" }
- chartData = []
- [:week, :month, :year].each do |scope|
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 4ad37d0e882..877101b05ca 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -20,4 +20,5 @@
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 2174154b207..ffb0ae95f9b 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -13,4 +13,3 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('pipelines_details')
diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml
index 74f7f63c941..6b284fda35c 100644
--- a/app/views/projects/protected_tags/_index.html.haml
+++ b/app/views/projects/protected_tags/_index.html.haml
@@ -1,6 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('protected_tags')
-
- content_for :create_protected_tag do
= render 'projects/protected_tags/create_protected_tag'
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 6e105a5521a..1827a3d323c 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -43,4 +43,5 @@
.form-actions
= button_tag s_('TagsPage|Create tag'), class: 'btn btn-create'
= link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 3312254f5fb..014b8de1dc9 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -7,6 +7,7 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
+ -# haml-lint:disable InlineJavaScript
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index c1589027898..adaddda13eb 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,6 +1,4 @@
- todo = issuable_todo(issuable)
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
@@ -119,10 +117,12 @@
= render partial: "shared/issuable/label_page_create"
- if issuable.has_attribute?(:confidential)
+ -# haml-lint:disable InlineJavaScript
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point
- if issuable.has_attribute?(:discussion_locked)
+ -# haml-lint:disable InlineJavaScript
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
@@ -159,4 +159,5 @@
= _('Move')
= icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
+ -# haml-lint:disable InlineJavaScript
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index cd4188daf5b..a942ebc328b 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -1,7 +1,5 @@
- affix_offset = local_assigns.fetch(:affix_offset, "50")
- project = local_assigns[:project]
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_vue')
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 0ceca367883..1db7c4e67cf 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -35,4 +35,5 @@
is locked. Only
%b project members
can comment.
+-# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index f878bece2fa..7eb221620ad 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -1,6 +1,7 @@
#js-authenticate-u2f
%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
+-# haml-lint:disable InlineJavaScript
%script#js-authenticate-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index 79e8f8d0e89..cc0e93c0755 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -1,5 +1,6 @@
#js-register-u2f
+-# haml-lint:disable InlineJavaScript
%script#js-register-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 28a5e5da037..a9415410f8a 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -84,6 +84,7 @@
- new_note
- pages
- pages_domain_verification
+- plugin
- post_receive
- process_commit
- project_cache
diff --git a/app/workers/plugin_worker.rb b/app/workers/plugin_worker.rb
new file mode 100644
index 00000000000..bfcc683d99a
--- /dev/null
+++ b/app/workers/plugin_worker.rb
@@ -0,0 +1,15 @@
+class PluginWorker
+ include ApplicationWorker
+
+ sidekiq_options retry: false
+
+ def perform(file_name, data)
+ success, message = Gitlab::Plugin.execute(file_name, data)
+
+ unless success
+ Gitlab::PluginLogger.error("Plugin Error => #{file_name}: #{message}")
+ end
+
+ true
+ end
+end
diff --git a/changelogs/unreleased/dz-system-hooks-plugins.yml b/changelogs/unreleased/dz-system-hooks-plugins.yml
new file mode 100644
index 00000000000..e6eb1dfb03b
--- /dev/null
+++ b/changelogs/unreleased/dz-system-hooks-plugins.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to use external plugins as an alternative to system hooks
+merge_request: 17003
+author:
+type: added
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index f037e3d1221..4845dc28a4a 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -68,3 +68,4 @@
- [project_migrate_hashed_storage, 1]
- [storage_migrator, 1]
- [pages_domain_verification, 1]
+ - [plugin, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index fb0a2c84ccc..e806083bf16 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -1,33 +1,33 @@
'use strict';
-var crypto = require('crypto');
-var fs = require('fs');
-var path = require('path');
-var glob = require('glob');
-var webpack = require('webpack');
-var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
-var CopyWebpackPlugin = require('copy-webpack-plugin');
-var CompressionPlugin = require('compression-webpack-plugin');
-var NameAllModulesPlugin = require('name-all-modules-plugin');
-var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
-var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
-
-var ROOT_PATH = path.resolve(__dirname, '..');
-var IS_PRODUCTION = process.env.NODE_ENV === 'production';
-var IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
-var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
-var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
-var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
-var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
-var NO_COMPRESSION = process.env.NO_COMPRESSION;
-
-var autoEntriesCount = 0;
-var watchAutoEntries = [];
+const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+const webpack = require('webpack');
+const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const CompressionPlugin = require('compression-webpack-plugin');
+const NameAllModulesPlugin = require('name-all-modules-plugin');
+const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
+const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
+
+const ROOT_PATH = path.resolve(__dirname, '..');
+const IS_PRODUCTION = process.env.NODE_ENV === 'production';
+const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
+const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
+const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
+const DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
+const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
+const NO_COMPRESSION = process.env.NO_COMPRESSION;
+
+let autoEntriesCount = 0;
+let watchAutoEntries = [];
function generateEntries() {
// generate automatic entry points
- var autoEntries = {};
- var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
+ const autoEntries = {};
+ const pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
watchAutoEntries = [
path.join(ROOT_PATH, 'app/assets/javascripts/pages/'),
];
@@ -44,22 +44,15 @@ function generateEntries() {
const manualEntries = {
balsamiq_viewer: './blob/balsamiq_viewer.js',
- environments: './environments/environments_bundle.js',
- filtered_search: './filtered_search/filtered_search_bundle.js',
- help: './help/help.js',
monitoring: './monitoring/monitoring_bundle.js',
mr_notes: './mr_notes/index.js',
notebook_viewer: './blob/notebook_viewer.js',
pdf_viewer: './blob/pdf_viewer.js',
- pipelines_details: './pipelines/pipeline_details_bundle.js',
- project_import_gl: './projects/project_import_gitlab_project.js',
protected_branches: './protected_branches',
- protected_tags: './protected_tags',
registry_list: './registry/index.js',
sketch_viewer: './blob/sketch_viewer.js',
stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js',
- ui_development_kit: './ui_development_kit.js',
two_factor_auth: './two_factor_auth.js',
common: './commons/index.js',
@@ -76,7 +69,7 @@ function generateEntries() {
return Object.assign(manualEntries, autoEntries);
}
-var config = {
+const config = {
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
entry: generateEntries,
@@ -168,7 +161,7 @@ var config = {
new StatsWriterPlugin({
filename: 'manifest.json',
transform: function(data, opts) {
- var stats = opts.compiler.getStats().toJson({
+ const stats = opts.compiler.getStats().toJson({
chunkModules: false,
source: false,
chunks: false,
diff --git a/doc/administration/plugins.md b/doc/administration/plugins.md
new file mode 100644
index 00000000000..c91ac3012b9
--- /dev/null
+++ b/doc/administration/plugins.md
@@ -0,0 +1,66 @@
+# Plugins
+
+**Note:** Plugins must be configured on the filesystem of the GitLab
+server. Only GitLab server administrators will be able to complete these tasks.
+Please explore [system hooks] or [webhooks] as an option if you do not
+have filesystem access.
+
+Introduced in GitLab 10.6.
+
+A plugin will run on each event so it's up to you to filter events or projects within a plugin code. You can have as many plugins as you want. Each plugin will be triggered by GitLab asynchronously in case of an event. For a list of events please see [system hooks] documentation.
+
+## Setup
+
+Plugins must be placed directly into `plugins` directory, subdirectories will be ignored.
+There is an `example` directory inside `plugins` where you can find some basic examples.
+
+Follow the steps below to set up a custom hook:
+
+1. On the GitLab server, navigate to the project's plugin directory.
+ For an installation from source the path is usually
+ `/home/git/gitlab/plugins/`. For Omnibus installs the path is
+ usually `/opt/gitlab/embedded/service/gitlab-rails/plugins`.
+1. Inside the `plugins` directory, create a file with a name of your choice, but without spaces or special characters.
+1. Make the hook file executable and make sure it's owned by the git user.
+1. Write the code to make the plugin function as expected. Plugin can be
+ in any language. Ensure the 'shebang' at the top properly reflects the language
+ type. For example, if the script is in Ruby the shebang will probably be
+ `#!/usr/bin/env ruby`.
+1. The data to the plugin will be provided as JSON on STDIN. It will be exactly same as one for [system hooks]
+
+That's it! Assuming the plugin code is properly implemented the hook will fire
+as appropriate. Plugins file list is updated for each event. There is no need to restart GitLab to apply a new plugin.
+
+If a plugin executes with non-zero exit code or GitLab fails to execute it, a
+message will be logged to `plugin.log`.
+
+## Validation
+
+Writing own plugin can be tricky and its easier if you can check it without altering the system.
+We provided a rake task you can use with staging environment to test your plugin before using it in production.
+The rake task will use a sample data and execute each of plugins. By output you should be able to determine if
+system sees your plugin and if it was executed without errors.
+
+```bash
+# Omnibus installations
+sudo gitlab-rake plugins:validate
+
+# Installations from source
+bundle exec rake plugins:validate RAILS_ENV=production
+```
+
+Example of output can be next:
+
+```
+-> bundle exec rake plugins:validate RAILS_ENV=production
+Validating plugins from /plugins directory
+* /home/git/gitlab/plugins/save_to_file.clj succeed (zero exit code)
+* /home/git/gitlab/plugins/save_to_file.rb failure (non-zero exit code)
+```
+
+[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
+[system hooks]: ../system_hooks/system_hooks.md
+[webhooks]: ../user/project/integrations/webhooks.md
+[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
+[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93
+
diff --git a/lib/gitlab/plugin.rb b/lib/gitlab/plugin.rb
new file mode 100644
index 00000000000..0d1cb16b378
--- /dev/null
+++ b/lib/gitlab/plugin.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Plugin
+ def self.files
+ Dir.glob(Rails.root.join('plugins/*')).select do |entry|
+ File.file?(entry)
+ end
+ end
+
+ def self.execute_all_async(data)
+ args = files.map { |file| [file, data] }
+
+ PluginWorker.bulk_perform_async(args)
+ end
+
+ def self.execute(file, data)
+ result = Gitlab::Popen.popen_with_detail([file]) do |stdin|
+ stdin.write(data.to_json)
+ end
+
+ exit_status = result.status&.exitstatus
+ [exit_status.zero?, result.stderr]
+ rescue => e
+ [false, e.message]
+ end
+ end
+end
diff --git a/lib/gitlab/plugin_logger.rb b/lib/gitlab/plugin_logger.rb
new file mode 100644
index 00000000000..c4f6ec3e21d
--- /dev/null
+++ b/lib/gitlab/plugin_logger.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ class PluginLogger < Gitlab::Logger
+ def self.file_name_noext
+ 'plugin'
+ end
+ end
+end
diff --git a/lib/haml_lint/inline_javascript.rb b/lib/haml_lint/inline_javascript.rb
index f5485eb89fa..4f776330e80 100644
--- a/lib/haml_lint/inline_javascript.rb
+++ b/lib/haml_lint/inline_javascript.rb
@@ -12,6 +12,12 @@ unless Rails.env.production?
record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
end
+
+ def visit_tag(node)
+ return unless node.tag_name == 'script'
+
+ record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
+ end
end
end
end
diff --git a/lib/tasks/plugins.rake b/lib/tasks/plugins.rake
new file mode 100644
index 00000000000..e73dd7e68df
--- /dev/null
+++ b/lib/tasks/plugins.rake
@@ -0,0 +1,16 @@
+namespace :plugins do
+ desc 'Validate existing plugins'
+ task validate: :environment do
+ puts 'Validating plugins from /plugins directory'
+
+ Gitlab::Plugin.files.each do |file|
+ success, message = Gitlab::Plugin.execute(file, Gitlab::DataBuilder::Push::SAMPLE_DATA)
+
+ if success
+ puts "* #{file} succeed (zero exit code)."
+ else
+ puts "* #{file} failure (non-zero exit code). #{message}"
+ end
+ end
+ end
+end
diff --git a/plugins/examples/save_to_file.clj b/plugins/examples/save_to_file.clj
new file mode 100755
index 00000000000..a59d83749d3
--- /dev/null
+++ b/plugins/examples/save_to_file.clj
@@ -0,0 +1,3 @@
+#!/usr/bin/env clojure
+(let [in (slurp *in*)]
+ (spit "/tmp/clj-data.txt" in))
diff --git a/plugins/examples/save_to_file.rb b/plugins/examples/save_to_file.rb
new file mode 100755
index 00000000000..61b0df9bfd6
--- /dev/null
+++ b/plugins/examples/save_to_file.rb
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+x = STDIN.read
+File.write('/tmp/rb-data.txt', x)
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 94fcc6c7f2b..fb4946aeeea 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -126,7 +126,6 @@ if (process.env.BABEL_ENV === 'coverage') {
'./diff_notes/components/resolve_count.js',
'./dispatcher.js',
'./environments/environments_bundle.js',
- './filtered_search/filtered_search_bundle.js',
'./graphs/graphs_bundle.js',
'./issuable/time_tracking/time_tracking_bundle.js',
'./main.js',
diff --git a/spec/lib/gitlab/plugin_spec.rb b/spec/lib/gitlab/plugin_spec.rb
new file mode 100644
index 00000000000..33dd4f79130
--- /dev/null
+++ b/spec/lib/gitlab/plugin_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe Gitlab::Plugin do
+ describe '.execute' do
+ let(:data) { Gitlab::DataBuilder::Push::SAMPLE_DATA }
+ let(:plugin) { Rails.root.join('plugins', 'test.rb') }
+ let(:tmp_file) { Tempfile.new('plugin-dump') }
+ let(:result) { described_class.execute(plugin.to_s, data) }
+ let(:success) { result.first }
+ let(:message) { result.last }
+
+ let(:plugin_source) do
+ <<~EOS
+ #!/usr/bin/env ruby
+ x = STDIN.read
+ File.write('#{tmp_file.path}', x)
+ EOS
+ end
+
+ before do
+ File.write(plugin, plugin_source)
+ end
+
+ after do
+ FileUtils.rm(plugin)
+ end
+
+ context 'successful execution' do
+ before do
+ File.chmod(0o777, plugin)
+ end
+
+ after do
+ tmp_file.close!
+ end
+
+ it { expect(success).to be true }
+ it { expect(message).to be_empty }
+
+ it 'ensures plugin received data via stdin' do
+ result
+
+ expect(File.read(tmp_file.path)).to eq(data.to_json)
+ end
+ end
+
+ context 'non-executable' do
+ it { expect(success).to be false }
+ it { expect(message).to include('Permission denied') }
+ end
+
+ context 'non-zero exit' do
+ let(:plugin_source) do
+ <<~EOS
+ #!/usr/bin/env ruby
+ exit 1
+ EOS
+ end
+
+ before do
+ File.chmod(0o777, plugin)
+ end
+
+ it { expect(success).to be false }
+ it { expect(message).to be_empty }
+ end
+ end
+end
diff --git a/spec/migrations/migrate_stages_statuses_spec.rb b/spec/migrations/migrate_stages_statuses_spec.rb
index 79d2708f9ad..ce35276cbf5 100644
--- a/spec/migrations/migrate_stages_statuses_spec.rb
+++ b/spec/migrations/migrate_stages_statuses_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170711145558_migrate_stages_statuses.rb')
-describe MigrateStagesStatuses, :migration do
+describe MigrateStagesStatuses, :sidekiq, :migration do
let(:jobs) { table(:ci_builds) }
let(:stages) { table(:ci_stages) }
let(:pipelines) { table(:ci_pipelines) }
diff --git a/spec/migrations/schedule_build_stage_migration_spec.rb b/spec/migrations/schedule_build_stage_migration_spec.rb
index 06657410173..e2ca35447fb 100644
--- a/spec/migrations/schedule_build_stage_migration_spec.rb
+++ b/spec/migrations/schedule_build_stage_migration_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180212101928_schedule_build_stage_migration')
-describe ScheduleBuildStageMigration, :migration do
+describe ScheduleBuildStageMigration, :sidekiq, :migration do
let(:projects) { table(:projects) }
let(:pipelines) { table(:ci_pipelines) }
let(:stages) { table(:ci_stages) }
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 4021e537efc..72cafac3f90 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -700,7 +700,7 @@ describe API::Runner do
context 'when tace is given' do
it 'creates a trace artifact' do
- allow_any_instance_of(BuildFinishedWorker).to receive(:perform).with(job.id) do
+ allow(BuildFinishedWorker).to receive(:perform_async).with(job.id) do
CreateTraceArtifactWorker.new.perform(job.id)
end
diff --git a/spec/support/gitlab-git-test.git/packed-refs b/spec/support/gitlab-git-test.git/packed-refs
index 507e4ce785a..ea50e4ad3f6 100644
--- a/spec/support/gitlab-git-test.git/packed-refs
+++ b/spec/support/gitlab-git-test.git/packed-refs
@@ -1,4 +1,4 @@
-# pack-refs with: peeled fully-peeled
+# pack-refs with: peeled fully-peeled sorted
0b4bc9a49b562e85de7cc9e834518ea6828729b9 refs/heads/feature
12d65c8dd2b2676fa3ac47d955accc085a37a9c1 refs/heads/fix
6473c90867124755509e100d0d35ebdc85a0b6ae refs/heads/fix-blob-path
diff --git a/spec/workers/plugin_worker_spec.rb b/spec/workers/plugin_worker_spec.rb
new file mode 100644
index 00000000000..9238a8199bc
--- /dev/null
+++ b/spec/workers/plugin_worker_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe PluginWorker do
+ include RepoHelpers
+
+ let(:filename) { 'my_plugin.rb' }
+ let(:data) { { 'event_name' => 'project_create' } }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'executes Gitlab::Plugin with expected values' do
+ allow(Gitlab::Plugin).to receive(:execute).with(filename, data).and_return([true, ''])
+
+ expect(subject.perform(filename, data)).to be_truthy
+ end
+
+ it 'logs message in case of plugin execution failure' do
+ allow(Gitlab::Plugin).to receive(:execute).with(filename, data).and_return([false, 'permission denied'])
+
+ expect(Gitlab::PluginLogger).to receive(:error)
+ expect(subject.perform(filename, data)).to be_truthy
+ end
+ end
+end