├── .gitignore
├── README.md
├── Vagrantfile
├── provisioning
├── files
│ ├── jenkins
│ │ ├── Drupal7-phing.xml
│ │ ├── Drupal7.xml
│ │ ├── Drupal8-phing.xml
│ │ └── Drupal8.xml
│ ├── sonar-php-custom-rules.sql
│ ├── sonar-php-profile-setup.sql
│ └── sonar
│ │ ├── sonar-7.properties
│ │ └── sonar-8.properties
├── host_vars
│ └── drupalci
├── playbook.yml
├── tasks
│ ├── drupal-cs.yml
│ ├── jenkins-setup.yml
│ ├── php-utils-composer.yml
│ └── sonar-php.yml
├── templates
│ └── jenkins
│ │ ├── hudson.plugins.sonar.SonarPublisher.xml.j2
│ │ └── hudson.plugins.sonar.SonarRunnerInstallation.xml.j2
└── vars
│ └── main.yml
├── requirements.yml
└── screenshot.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vagrant/
3 | vagrant_ansible_inventory_default
4 |
5 | # Prying eyes...
6 | inventory
7 | **/drupalci.midwesternmac.com
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DEPRECATED - Jenkins and SonarQube Drupal CI and Static Code Analysis
2 |
3 | > **DEPRECATION NOTICE**: This project has been deprecated as of 2018; please see [Issue #27: Deprecate this project](https://github.com/geerlingguy/drupalci-sonar-jenkins/issues/27) for details and further discussion.
4 |
5 |
6 |
7 | This Vagrant configuration (with Ansible for provisioning) will install Jenkins, PHP, SonarQube, and Drupal CI profiles for code analysis (along with a bunch of other required software).
8 |
9 | How is this helpful? It's easy to track things like code complexity, lines of code, comment percentage, coding standards compliance, and test coverage over time. Code quality helps make Drupal more maintainable, especially as the project continues to grow!
10 |
11 | ## Quick Start Guide
12 |
13 | ### 1 - Install dependencies (VirtualBox, Vagrant, Ansible)
14 |
15 | 1. Download and install [VirtualBox](https://www.virtualbox.org/wiki/Downloads).
16 | 2. Download and install [Vagrant](http://www.vagrantup.com/downloads.html).
17 | 3. [Mac/Linux only] Install [Ansible](http://docs.ansible.com/intro_installation.html).
18 | 4. Install Ansible roles: `ansible-galaxy install -r requirements.yml` (inside this directory).
19 |
20 | Note for Windows users: *This guide assumes you're on a Mac or Linux host. Windows support may be added when I get a little more time; the main difference is Ansible needs to be bootstrapped from within the VM after it's created. See [JJG-Ansible-Windows](https://github.com/geerlingguy/JJG-Ansible-Windows) for more information.*
21 |
22 | ### 2 - Build the Virtual Machine
23 |
24 | 1. Download this project and put it wherever you want.
25 | 2. Open Terminal, cd to this directory (containing the `Vagrantfile` and this REAMDE file).
26 | 3. Type in `vagrant up`, and let Vagrant do its magic.
27 |
28 | Note: *If there are any errors during the course of running `vagrant up`, and it drops you back to your command prompt, just run `vagrant provision` to continue building the VM from where you left off. If there are still errors after doing this a few times, post an issue to this project's issue queue on GitHub with the error.*
29 |
30 | ### 3 - Configure your host machine to access the VM.
31 |
32 | 1. [Edit your hosts file](http://www.rackspace.com/knowledge_center/article/how-do-i-modify-my-hosts-file), adding the line `192.168.99.9 drupalci.dev` so you can connect to the VMs.
33 | 2. Open your browser and access [http://drupalci.dev/](http://drupalci.dev/).
34 |
35 | ## Notes
36 |
37 | - If you're running this on a production server (visible to the Internet), make sure you configure Jenkins and Sonar security, and set secret/complex MySQL passwords for both the root and sonar users! (When setting up Jenkins security, be careful to not lock yourself out; if you do, you need to edit the Jenkins config.xml file and restart Jenkins).
38 | - To shut down the virtual machine, enter `vagrant halt` in the Terminal in the same folder that has the `Vagrantfile`. To destroy it completely (if you want to save a little disk space, or want to rebuild it from scratch with `vagrant up` again), type in `vagrant destroy`.
39 | - Find out more about local development with Vagrant + VirtualBox + Ansible in this presentation: [Local Development Environments - Vagrant, VirtualBox and Ansible](http://www.slideshare.net/geerlingguy/local-development-on-virtual-machines-vagrant-virtualbox-and-ansible).
40 | - Learn about how Ansible can accelerate your ability to innovate and manage your infrastructure by reading [Ansible for DevOps](http://www.ansiblefordevops.com/).
41 |
42 | ## Author Information
43 |
44 | This project was created in 2014 by [Jeff Geerling](https://jeffgeerling.com/), author of [Ansible for DevOps](https://www.ansiblefordevops.com/).
45 |
--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | VAGRANTFILE_API_VERSION = "2"
5 |
6 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
7 | config.vm.box = "geerlingguy/centos6"
8 | config.ssh.insert_key = false
9 |
10 | config.vm.provider :virtualbox do |vb|
11 | vb.customize ["modifyvm", :id, "--name", "drupalci.dev"]
12 | vb.customize ["modifyvm", :id, "--memory", 2048]
13 | vb.customize ["modifyvm", :id, "--cpus", 2]
14 | vb.customize ["modifyvm", :id, "--ioapic", "on"]
15 | end
16 |
17 | if Vagrant.has_plugin?('vagrant-auto_network')
18 | vagrant_ip = '0.0.0.0'
19 | config.vm.network :private_network, ip: vagrant_ip, auto_network: true
20 | else
21 | vagrant_ip = '192.168.99.9'
22 | config.vm.network :private_network, ip: vagrant_ip
23 | end
24 |
25 | config.vm.hostname = "drupalci.dev"
26 | aliases = [config.vm.hostname, vagrant_ip]
27 |
28 | if Vagrant.has_plugin?('vagrant-hostsupdater')
29 | config.hostsupdater.aliases = aliases
30 | elsif Vagrant.has_plugin?('vagrant-hostmanager')
31 | config.hostmanager.enabled = true
32 | config.hostmanager.manage_host = true
33 | config.hostmanager.aliases = aliases
34 | end
35 |
36 | # Set the name of the VM. See: http://stackoverflow.com/a/17864388/100134
37 | config.vm.define :drupalci do |drupalci_config|
38 | end
39 |
40 | # Enable Ansible provisioner.
41 | config.vm.provision :ansible do |ansible|
42 | ansible.playbook = "provisioning/playbook.yml"
43 | ansible.sudo = true
44 | ansible.extra_vars = {
45 | vagrant_ip: vagrant_ip
46 | }
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/provisioning/files/jenkins/Drupal7-phing.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/provisioning/files/jenkins/Drupal7.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 | 2
9 |
10 |
11 | http://git.drupal.org/project/drupal.git
12 |
13 |
14 |
15 |
16 | 7.x
17 |
18 |
19 | false
20 |
21 |
22 |
23 | drupal
24 |
25 |
26 |
27 |
28 | true
29 | false
30 | false
31 | false
32 | (Inherit From Job)
33 |
34 |
35 | H H * * *
36 |
37 |
38 | false
39 |
40 |
41 | /etc/sonar/sonar-7.properties
42 |
43 |
44 | (Inherit From Job)
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/provisioning/files/jenkins/Drupal8-phing.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
31 |
34 |
35 |
36 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/provisioning/files/jenkins/Drupal8.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | false
6 |
7 |
8 | 2
9 |
10 |
11 | http://git.drupal.org/project/drupal.git
12 |
13 |
14 |
15 |
16 | 8.0.x
17 |
18 |
19 | false
20 |
21 |
22 |
23 | drupal
24 |
25 |
26 |
27 |
28 | true
29 | false
30 | false
31 | false
32 | (Inherit From Job)
33 |
34 |
35 | H H * * *
36 |
37 |
38 | false
39 |
40 |
41 | /etc/sonar/sonar-8.properties
42 |
43 |
44 | (Inherit From Job)
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/provisioning/files/sonar-php-custom-rules.sql:
--------------------------------------------------------------------------------
1 | # Add custom PHP CodeSniffer rules for Drupal into SonarQube.
2 | #
3 | # This must be run AFTER SonarQube has been installed and started, so the MySQL
4 | # database schema exists.
5 | #
6 | # Command: mysql -u root -ppass sonar < sonar-php-custom-rules.sql
7 | #
8 | # See: https://drupal.org/node/2082563
9 |
10 | # Insert new 'Drupal' ruleset (should be id 8).
11 | INSERT INTO rules_profiles VALUES (NULL, 'Drupal', 'php', 'All PHPMD Rules', 1, 0);
12 |
13 | # Insert custom rules for PHP Codesniffer into the properties table.
14 | INSERT INTO properties VALUES (NULL, 'sonar.phpCodesniffer.customRules.definition', NULL, '
15 |
16 |
17 | Drupal.Commenting.FunctionComment.ParamCommentNewLine
18 | PARAMCOMMENTNEWLINE
19 | Drupal.Commenting.FunctionComment.ParamCommentNewLine
20 |
21 |
22 | Drupal.Commenting.FunctionComment.HookReturnDoc
23 | HOOKRETURNDOC
24 | Drupal.Commenting.FunctionComment.HookReturnDoc
25 |
26 |
27 | Drupal.Commenting.FunctionComment.MissingReturnType
28 | MISSINGRETURNTYPE
29 | Drupal.Commenting.FunctionComment.MissingReturnType
30 |
31 |
32 | Drupal.Commenting.FunctionComment.HookParamDoc
33 | HOOKPARAMDOC
34 | Drupal.Commenting.FunctionComment.HookParamDoc
35 |
36 |
37 | Drupal.Commenting.FunctionComment.ParamNameNoMatch
38 | PARAMNAMENOMATCH
39 | Drupal.Commenting.FunctionComment.ParamNameNoMatch
40 |
41 |
42 | Drupal.Commenting.FunctionComment.SpacingBeforeParamType
43 | SPACINGBEFOREPARAMTYPE
44 | Drupal.Commenting.FunctionComment.SpacingBeforeParamType
45 |
46 |
47 | Drupal.Commenting.InlineComment.SpacingBefore
48 | SPACINGBEFORE
49 | Drupal.Commenting.InlineComment.SpacingBefore
50 |
51 |
52 | Drupal.Commenting.FileComment
53 | FILECOMMENT
54 | Drupal.Commenting.FileComment
55 |
56 |
57 | Drupal.ControlStructures.InlineControlStructure.NotAllowed
58 | NOTALLOWED
59 | Drupal.ControlStructures.InlineControlStructure.NotAllowed
60 |
61 |
62 | Drupal.Commenting.FunctionComment
63 | FUNCTIONCOMMENT
64 | Drupal.Commenting.FunctionComment
65 |
66 |
67 | Drupal.Files.LineLength.TooLong
68 | TOOLONG
69 | Drupal.Files.LineLength.TooLong
70 |
71 |
72 | Drupal.Semantics.FunctionCall.LArg
73 | LARG
74 | Drupal.Semantics.FunctionCall.LArg
75 |
76 |
77 | Drupal.WhiteSpace.OperatorSpacing.SpacingAfter
78 | SPACINGAFTER
79 | Drupal.WhiteSpace.OperatorSpacing.SpacingAfter
80 |
81 |
82 | Drupal.Formatting.DisallowCloseTag.FinalClose
83 | FINALCLOSE
84 | Drupal.Formatting.DisallowCloseTag.FinalClose
85 |
86 |
87 | Drupal.WhiteSpace.OpenBracketSpacing.OpeningWhitespace
88 | OPENINGWHITESPACE
89 | Drupal.WhiteSpace.OpenBracketSpacing.OpeningWhitespace
90 |
91 |
92 | Drupal.Commenting.FileComment.DescriptionMissing
93 | DESCRIPTIONMISSING
94 | Drupal.Commenting.FileComment.DescriptionMissing
95 |
96 |
97 | Drupal.Commenting.InlineComment.NoSpaceBefore
98 | NOSPACEBEFORE
99 | Drupal.Commenting.InlineComment.NoSpaceBefore
100 |
101 |
102 | Drupal.Strings.ConcatenationSpacing.Missing
103 | MISSING
104 | Drupal.Strings.ConcatenationSpacing.Missing
105 |
106 |
107 | Drupal.Commenting.InlineComment.WrongStyle
108 | WRONGSTYLE
109 | Drupal.Commenting.InlineComment.WrongStyle
110 |
111 |
112 | Drupal.Semantics.Br.XHTMLBr
113 | XHTMLBR
114 | Drupal.Semantics.Br.XHTMLBr
115 |
116 |
117 | Drupal.Strings.UnnecessaryStringConcat.Found
118 | FOUND
119 | Drupal.Strings.UnnecessaryStringConcat.Found
120 |
121 |
122 | Drupal.Commenting.FunctionComment.Empty
123 | EMPTY
124 | Drupal.Commenting.FunctionComment.Empty
125 |
126 |
127 | Drupal.NamingConventions.ValidClassName.NoUnderscores
128 | NOUNDERSCORES
129 | Drupal.NamingConventions.ValidClassName.NoUnderscores
130 |
131 |
132 | Drupal.Commenting.FileComment.SpacingAfter
133 | SPACINGAFTER
134 | Drupal.Commenting.FileComment.SpacingAfter
135 |
136 |
137 | Drupal.Commenting.FunctionComment.$InReturnType
138 | $INRETURNTYPE
139 | Drupal.Commenting.FunctionComment.$InReturnType
140 |
141 |
142 | Drupal.Array.Array.LongLineDeclaration
143 | LONGLINEDECLARATION
144 | Drupal.Array.Array.LongLineDeclaration
145 |
146 |
147 | Drupal.Commenting.DocCommentAlignment.SpaceBeforeAsterisk
148 | SPACEBEFOREASTERISK
149 | Drupal.Commenting.DocCommentAlignment.SpaceBeforeAsterisk
150 |
151 |
152 | Drupal.Commenting.FunctionComment.ShortSingleLine
153 | SHORTSINGLELINE
154 | Drupal.Commenting.FunctionComment.ShortSingleLine
155 |
156 |
157 | Drupal.Semantics.FunctionCall.NotLiteralString
158 | NOTLITERALSTRING
159 | Drupal.Semantics.FunctionCall.NotLiteralString
160 |
161 |
162 | Drupal.Commenting.FunctionComment.ShortFullStop
163 | SHORTFULLSTOP
164 | Drupal.Commenting.FunctionComment.ShortFullStop
165 |
166 |
167 | Drupal.Commenting.InlineComment.NotCapital
168 | NOTCAPITAL
169 | Drupal.Commenting.InlineComment.NotCapital
170 |
171 |
172 | Drupal.Commenting.InlineComment.SpacingAfter
173 | SPACINGAFTER
174 | Drupal.Commenting.InlineComment.SpacingAfter
175 |
176 |
177 | Drupal.Commenting.FileComment.WrongStyle
178 | WRONGSTYLE
179 | Drupal.Commenting.FileComment.WrongStyle
180 |
181 |
182 | Drupal.Formatting.SpaceInlineIf.NoSpaceAfter
183 | NOSPACEAFTER
184 | Drupal.Formatting.SpaceInlineIf.NoSpaceAfter
185 |
186 |
187 | Drupal.Commenting.FunctionComment.ShortNotCapital
188 | SHORTNOTCAPITAL
189 | Drupal.Commenting.FunctionComment.ShortNotCapital
190 |
191 |
192 | Drupal.Array.Array.ArrayIndentation
193 | ARRAYINDENTATION
194 | Drupal.Array.Array.ArrayIndentation
195 |
196 |
197 | Drupal.Commenting.FunctionComment.MissingParamType
198 | MISSINGPARAMTYPE
199 | Drupal.Commenting.FunctionComment.MissingParamType
200 |
201 |
202 | Drupal.ControlStructures.ElseIf
203 | ELSEIF
204 | Drupal.ControlStructures.ElseIf
205 |
206 |
207 | Drupal.Array.Array
208 | ARRAY
209 | Drupal.Array.Array
210 |
211 |
212 | Drupal.ControlStructures.TemplateControlStructure.CurlyBracket
213 | CURLYBRACKET
214 | Drupal.ControlStructures.TemplateControlStructure.CurlyBracket
215 |
216 |
217 | Drupal.Array.Array.ArrayClosingIndentation
218 | ARRAYCLOSINGINDENTATION
219 | Drupal.Array.Array.ArrayClosingIndentation
220 |
221 |
222 | Drupal.WhiteSpace.ScopeIndent.IncorrectExact
223 | INCORRECTEXACT
224 | Drupal.WhiteSpace.ScopeIndent.IncorrectExact
225 |
226 |
227 | Drupal.ControlStructures.ControlSignature
228 | CONTROLSIGNATURE
229 | Drupal.ControlStructures.ControlSignature
230 |
231 |
232 | Drupal.Formatting.SpaceInlineIf.NoSpaceBefore
233 | NOSPACEBEFORE
234 | Drupal.Formatting.SpaceInlineIf.NoSpaceBefore
235 |
236 |
237 | Drupal.Semantics.EmptyInstall.EmptyInstall
238 | EMPTYINSTALL
239 | Drupal.Semantics.EmptyInstall.EmptyInstall
240 |
241 |
242 | Drupal.Commenting.FunctionComment.ShortStartSpace
243 | SHORTSTARTSPACE
244 | Drupal.Commenting.FunctionComment.ShortStartSpace
245 |
246 |
247 | Drupal.WhiteSpace.OperatorSpacing.NoSpaceAfter
248 | NOSPACEAFTER
249 | Drupal.WhiteSpace.OperatorSpacing.NoSpaceAfter
250 |
251 |
252 | Drupal.ControlStructures.ElseCatchNewline.ElseNewline
253 | ELSENEWLINE
254 | Drupal.ControlStructures.ElseCatchNewline.ElseNewline
255 |
256 |
257 | Drupal.Commenting.InlineComment.InvalidEndChar
258 | INVALIDENDCHAR
259 | Drupal.Commenting.InlineComment.InvalidEndChar
260 |
261 |
262 | Drupal.WhiteSpace.CloseBracketSpacing.ClosingWhitespace
263 | CLOSINGWHITESPACE
264 | Drupal.WhiteSpace.CloseBracketSpacing.ClosingWhitespace
265 |
266 |
267 | Drupal.Semantics.TInHookMenu.TFound
268 | TFOUND
269 | Drupal.Semantics.TInHookMenu.TFound
270 |
271 |
272 | Drupal.Commenting.FunctionComment.Missing
273 | MISSING
274 | Drupal.Commenting.FunctionComment.Missing
275 |
276 |
277 | Drupal.Semantics.FunctionCall.EmptyString
278 | EMPTYSTRING
279 | Drupal.Semantics.FunctionCall.EmptyString
280 |
281 |
282 | Drupal.Semantics.FunctionCall.ConstantStart
283 | CONSTANTSTART
284 | Drupal.Semantics.FunctionCall.ConstantStart
285 |
286 |
287 | Drupal.Formatting.MultiLineAssignment
288 | MULTILINEASSIGNMENT
289 | Drupal.Formatting.MultiLineAssignment
290 |
291 |
292 | Drupal.Commenting.InlineComment.Empty
293 | EMPTY
294 | Drupal.Commenting.InlineComment.Empty
295 |
296 |
297 | Drupal.NamingConventions.ValidVariableName
298 | VALIDVARIABLENAME
299 | Drupal.NamingConventions.ValidVariableName
300 |
301 |
302 | Drupal.Commenting.DocCommentAlignment.BlankLine
303 | BLANKLINE
304 | Drupal.Commenting.DocCommentAlignment.BlankLine
305 |
306 |
307 | Drupal.WhiteSpace.OperatorSpacing.NoSpaceBefore
308 | NOSPACEBEFORE
309 | Drupal.WhiteSpace.OperatorSpacing.NoSpaceBefore
310 |
311 |
312 | Drupal.WhiteSpace.EmptyLines.EmptyLines
313 | EMPTYLINES
314 | Drupal.WhiteSpace.EmptyLines.EmptyLines
315 |
316 |
317 | Drupal.WhiteSpace.FileEnd.FileEnd
318 | FILEEND
319 | Drupal.WhiteSpace.FileEnd.FileEnd
320 |
321 |
322 | Drupal.WhiteSpace.ScopeIndent.Incorrect
323 | INCORRECT
324 | Drupal.WhiteSpace.ScopeIndent.Incorrect
325 |
326 |
327 | Drupal.WhiteSpace.ScopeClosingBrace.BreakIdent
328 | BREAKIDENT
329 | Drupal.WhiteSpace.ScopeClosingBrace.BreakIdent
330 |
331 |
332 | Drupal.WhiteSpace.ObjectOperatorSpacing.After
333 | AFTER
334 | Drupal.WhiteSpace.ObjectOperatorSpacing.After
335 |
336 |
337 | Drupal.NamingConventions.ValidClassName.StartWithCaptial
338 | STARTWITHCAPTIAL
339 | Drupal.NamingConventions.ValidClassName.StartWithCaptial
340 |
341 |
342 | Drupal.WhiteSpace.ScopeClosingBrace.Indent
343 | INDENT
344 | Drupal.WhiteSpace.ScopeClosingBrace.Indent
345 |
346 |
347 | Drupal.Commenting.FileComment.Missing
348 | MISSING
349 | Drupal.Commenting.FileComment.Missing
350 |
351 |
352 | Drupal.WhiteSpace.ObjectOperatorSpacing.Before
353 | BEFORE
354 | Drupal.WhiteSpace.ObjectOperatorSpacing.Before
355 |
356 |
357 | Drupal.Classes.ClassCreateInstance
358 | CLASSCREATEINSTANCE
359 | Drupal.Classes.ClassCreateInstance
360 |
361 |
362 | Drupal.Semantics.FunctionCall.FunctionAlias
363 | FUNCTIONALIAS
364 | Drupal.Semantics.FunctionCall.FunctionAlias
365 |
366 |
367 | Drupal.Commenting.FunctionComment.MissingParamComment
368 | MISSINGPARAMCOMMENT
369 | Drupal.Commenting.FunctionComment.MissingParamComment
370 |
371 |
372 | Drupal.Commenting.FunctionComment.EmptyLinesAfterDoc
373 | EMPTYLINESAFTERDOC
374 | Drupal.Commenting.FunctionComment.EmptyLinesAfterDoc
375 |
376 |
377 | Drupal.Semantics.FunctionCall.BackslashSingleQuote
378 | BACKSLASHSINGLEQUOTE
379 | Drupal.Semantics.FunctionCall.BackslashSingleQuote
380 |
381 |
382 | Drupal.Formatting.SpaceInlineIf.SpacingBefore
383 | SPACINGBEFORE
384 | Drupal.Formatting.SpaceInlineIf.SpacingBefore
385 |
386 |
387 | Drupal.Semantics.FunctionCall.Concat
388 | CONCAT
389 | Drupal.Semantics.FunctionCall.Concat
390 |
391 |
392 | Drupal.Semantics.FunctionCall.WatchdogT
393 | WATCHDOGT
394 | Drupal.Semantics.FunctionCall.WatchdogT
395 |
396 |
397 | Drupal.Commenting.FunctionComment.SeePunctuation
398 | SEEPUNCTUATION
399 | Drupal.Commenting.FunctionComment.SeePunctuation
400 |
401 |
402 | Drupal.Semantics.InstallT.TranslationFound
403 | TRANSLATIONFOUND
404 | Drupal.Semantics.InstallT.TranslationFound
405 |
406 |
407 | Drupal.Commenting.FunctionComment.MissingShort
408 | MISSINGSHORT
409 | Drupal.Commenting.FunctionComment.MissingShort
410 |
411 |
412 | Drupal.NamingConventions.ValidFunctionName.NotLowerCamel
413 | NOTLOWERCAMEL
414 | Drupal.NamingConventions.ValidFunctionName.NotLowerCamel
415 |
416 |
417 | Drupal.WhiteSpace.ScopeClosingBrace
418 | SCOPECLOSINGBRACE
419 | Drupal.WhiteSpace.ScopeClosingBrace
420 |
421 |
422 | Drupal.Commenting.FunctionComment.MissingReturnComment
423 | MISSINGRETURNCOMMENT
424 | Drupal.Commenting.FunctionComment.MissingReturnComment
425 |
426 |
427 | Drupal.Commenting.FunctionComment.ReturnCommentNewLine
428 | RETURNCOMMENTNEWLINE
429 | Drupal.Commenting.FunctionComment.ReturnCommentNewLine
430 |
431 |
432 | Drupal.Commenting.FunctionComment.MissingParamName
433 | MISSINGPARAMNAME
434 | Drupal.Commenting.FunctionComment.MissingParamName
435 |
436 |
437 | Drupal.Classes.ClassDeclaration
438 | CLASSDECLARATION
439 | Drupal.Classes.ClassDeclaration
440 |
441 |
442 | Drupal.WhiteSpace.OperatorSpacing.SpacingBefore
443 | SPACINGBEFORE
444 | Drupal.WhiteSpace.OperatorSpacing.SpacingBefore
445 |
446 |
447 | Drupal.NamingConventions.ValidVariableName.LowerCamelName
448 | LOWERCAMELNAME
449 | Drupal.NamingConventions.ValidVariableName.LowerCamelName
450 |
451 |
452 | Drupal.Commenting.InlineComment.DocBlock
453 | DOCBLOCK
454 | Drupal.Commenting.InlineComment.DocBlock
455 |
456 |
457 | Drupal.Commenting.FunctionComment.ContentAfterOpen
458 | CONTENTAFTEROPEN
459 | Drupal.Commenting.FunctionComment.ContentAfterOpen
460 |
461 |
462 | Drupal.Functions.FunctionDeclaration.SpaceBeforeParenthesis
463 | SPACEBEFOREPARENTHESIS
464 | Drupal.Functions.FunctionDeclaration.SpaceBeforeParenthesis
465 |
466 |
467 | Drupal.Commenting.FunctionComment.SpacingAfterParams
468 | SPACINGAFTERPARAMS
469 | Drupal.Commenting.FunctionComment.SpacingAfterParams
470 |
471 |
472 | Drupal.Commenting.FunctionComment.ExtraParamComment
473 | EXTRAPARAMCOMMENT
474 | Drupal.Commenting.FunctionComment.ExtraParamComment
475 |
476 |
477 | Drupal.Commenting.FunctionComment.SeeAdditionalText
478 | SEEADDITIONALTEXT
479 | Drupal.Commenting.FunctionComment.SeeAdditionalText
480 |
481 |
482 | Drupal.Commenting.FunctionComment.SpacingBeforeParams
483 | SPACINGBEFOREPARAMS
484 | Drupal.Commenting.FunctionComment.SpacingBeforeParams
485 |
486 |
487 | Drupal.Commenting.FunctionComment.InvalidParamTypeName
488 | INVALIDPARAMTYPENAME
489 | Drupal.Commenting.FunctionComment.InvalidParamTypeName
490 |
491 |
492 | Drupal.NamingConventions.ValidFunctionName.ScopeNotLowerCamel
493 | SCOPENOTLOWERCAMEL
494 | Drupal.NamingConventions.ValidFunctionName.ScopeNotLowerCamel
495 |
496 |
497 | Drupal.Commenting.DocCommentAlignment.SpaceBeforeTag
498 | SPACEBEFORETAG
499 | Drupal.Commenting.DocCommentAlignment.SpaceBeforeTag
500 |
501 |
502 | Drupal.Commenting.FunctionComment.ParamCommentIndentation
503 | PARAMCOMMENTINDENTATION
504 | Drupal.Commenting.FunctionComment.ParamCommentIndentation
505 |
506 |
507 | Drupal.Formatting.SpaceInlineIf.SpacingAfter
508 | SPACINGAFTER
509 | Drupal.Formatting.SpaceInlineIf.SpacingAfter
510 |
511 |
512 | Drupal.Commenting.FunctionComment.InvalidReturnTypeName
513 | INVALIDRETURNTYPENAME
514 | Drupal.Commenting.FunctionComment.InvalidReturnTypeName
515 |
516 |
517 | Drupal.Commenting.FunctionComment.VoidReturn
518 | VOIDRETURN
519 | Drupal.Commenting.FunctionComment.VoidReturn
520 |
521 |
522 | Drupal.Commenting.FunctionComment.SpacingBeforeReturnType
523 | SPACINGBEFORERETURNTYPE
524 | Drupal.Commenting.FunctionComment.SpacingBeforeReturnType
525 |
526 | ', NULL);
527 |
--------------------------------------------------------------------------------
/provisioning/files/sonar-php-profile-setup.sql:
--------------------------------------------------------------------------------
1 | # Finish PHP SonarQube profile setup after new SonarQube restarts.
2 | #
3 | # This must be run AFTER SonarQube has been restarted (after the new PHP rules
4 | # are added, so rules are in place in the proper tables.
5 | #
6 | # Command: mysql -u root -ppass sonar < sonar-php-profile-setup.sql
7 |
8 | # Add Drupal rules to active rule set (3 = Drupal profile).
9 | INSERT INTO active_rules
10 | SELECT NULL, 3, id, 2, NULL
11 | FROM rules
12 | WHERE language = 'php' AND plugin_rule_key REGEXP 'Drupal';
13 |
14 | # Make the Drupal 7 Rules the default profile for PHP projects.
15 | UPDATE properties SET text_value = 'Drupal' WHERE prop_key = 'sonar.profile.php';
16 |
--------------------------------------------------------------------------------
/provisioning/files/sonar/sonar-7.properties:
--------------------------------------------------------------------------------
1 | # Drupal 7 Sonar properties.
2 |
3 | # Required metadata
4 | sonar.projectKey=org.drupal.d7
5 | sonar.projectName=Drupal 7
6 | sonar.projectVersion=7.0
7 |
8 | # Description
9 | sonar.projectDescription=Drupal 7
10 |
11 | # Path to source directories (required)
12 | # (All paths are relative to Jenkins' 'workspace').
13 | sonar.sources=drupal
14 |
15 | # The language of the project.
16 | sonar.language=php
17 |
18 | # Encoding of the source code
19 | sonar.sourceEncoding=UTF-8
20 |
21 | # File suffixes to check
22 | sonar.php.file.suffixes=php,inc,module,install
23 |
24 | # PHPMD (not used currently)
25 | #sonar.phpPmd.analyzeOnly=true
26 | #sonar.phpPmd.reportPath=logs/php-md.xml
27 |
28 | # PHP CodeSniffer (not used currently)
29 | #sonar.phpCodesniffer.analyzeOnly=true
30 | #sonar.phpCodesniffer.reportPath=logs/codesniffer.xml
31 |
32 | # Set timeout much longer.
33 | sonar.phpDepend.timeout=240
34 | sonar.phpPmd.timeout=240
35 | sonar.phpCodesniffer.timeout=240
36 |
37 | # Files to exclude
38 | sonar.exclusions=**/*.features.*,\
39 | **/*.field_group.inc,\
40 | ***/*.strongarm.inc,\
41 | **/*.views_default.inc
42 |
--------------------------------------------------------------------------------
/provisioning/files/sonar/sonar-8.properties:
--------------------------------------------------------------------------------
1 | # Drupal 8 Sonar properties.
2 | #
3 | # TODO:
4 | # - Add PHPUnit test integration.
5 | # - Make sure everything that should be excluded is excluded.
6 | # - Find ways to speed up analysis. Analysis takes ~1.5 hours currently!
7 |
8 | # Required metadata
9 | sonar.projectKey=org.drupal.d8
10 | sonar.projectName=Drupal 8
11 | sonar.projectVersion=8.0
12 |
13 | # Description
14 | sonar.projectDescription=Drupal 8
15 |
16 | # Path to source directories (required)
17 | # (All paths are relative to Jenkins' 'workspace').
18 | sonar.sources=drupal
19 |
20 | # Path to tests (Sonar doesn't support wildcards for this property...).
21 | #sonar.tests=**/tests/*
22 |
23 | # The language of the project.
24 | sonar.language=php
25 |
26 | # Encoding of the source code
27 | sonar.sourceEncoding=UTF-8
28 |
29 | # File suffixes to check
30 | sonar.php.file.suffixes=php,inc,module,install
31 |
32 | # PHPMD (not used currently)
33 | #sonar.phpPmd.analyzeOnly=true
34 | #sonar.phpPmd.reportPath=logs/php-md.xml
35 |
36 | # PHP CodeSniffer (not used currently)
37 | #sonar.phpCodesniffer.analyzeOnly=true
38 | #sonar.phpCodesniffer.reportPath=logs/codesniffer.xml
39 |
40 | # Set timeouts much longer.
41 | sonar.phpDepend.timeout=480
42 | sonar.phpPmd.timeout=480
43 | sonar.phpCodesniffer.timeout=480
44 | sonar.phpUnit.timeout=240
45 |
46 | # Files to exclude
47 | sonar.exclusions=**/vendor/**,**/tests/**
48 | sonar.phpDepend.argumentLine=--ignore=vendor,tests --exclude=vendor,tests
49 | sonar.phpPmd.argumentLine=--exclude vendor,tests
50 | sonar.phpCodesniffer.argumentLine=--ignore=vendor,tests
51 |
--------------------------------------------------------------------------------
/provisioning/host_vars/drupalci:
--------------------------------------------------------------------------------
1 | ---
2 | mysql_root_password: root
3 |
4 | sonar_host: "{{ vagrant_ip }}"
5 | sonar_mysql_password: sonar
6 | sonar_mysql_allowed_hosts:
7 | - "{{ sonar_host }}"
8 | - 127.0.0.1
9 | - ::1
10 | - localhost
11 |
12 | jenkins_sonar_login: ""
13 | jenkins_sonar_password: ""
14 |
--------------------------------------------------------------------------------
/provisioning/playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # Drupal CI server with Jenkins and SonarQube.
3 | #
4 | # @author Jeff Geerling, 2014
5 | - hosts: drupalci
6 |
7 | vars_files:
8 | - vars/main.yml
9 |
10 | pre_tasks:
11 | - name: Ensure unzip is installed.
12 | yum: name=unzip state=present
13 |
14 | roles:
15 | - geerlingguy.firewall
16 | - geerlingguy.repo-epel
17 | - geerlingguy.repo-remi
18 | - geerlingguy.ntp
19 | - geerlingguy.munin-node
20 | - geerlingguy.git
21 | - geerlingguy.java
22 | - geerlingguy.jenkins
23 | - geerlingguy.php
24 | - geerlingguy.composer
25 | - geerlingguy.sonar-runner
26 | - geerlingguy.sonar
27 |
28 | tasks:
29 | - include: tasks/php-utils-composer.yml
30 | - include: tasks/drupal-cs.yml
31 | - include: tasks/jenkins-setup.yml
32 | - include: tasks/sonar-php.yml
33 |
34 | - name: Ensure Apache is not running.
35 | service: name=httpd state=stopped
36 |
--------------------------------------------------------------------------------
/provisioning/tasks/drupal-cs.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Download Drupal Coder module.
3 | get_url: >
4 | url=http://ftp.drupal.org/files/projects/coder-7.x-2.x-dev.tar.gz
5 | dest={{ workspace }}/coder.tar.gz
6 |
7 | - name: Expand Coder module archive.
8 | command: >
9 | tar -zxf {{ workspace }}/coder.tar.gz
10 | creates={{ workspace }}/coder/coder.info
11 | chdir={{ workspace }}
12 |
13 | - name: Move Drupal standards into CodeSniffer directory.
14 | command: >
15 | cp -r {{ workspace }}/coder/coder_sniffer/Drupal
16 | /var/lib/jenkins/.composer/vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/Drupal
17 | creates=/var/lib/jenkins/.composer/vendor/squizlabs/php_codesniffer/CodeSniffer/Standards/Drupal/ruleset.xml
18 | become: yes
19 |
20 | - name: Ensure Drupal Sonar config directory exists.
21 | file: >
22 | path=/etc/sonar
23 | state=directory
24 | mode=755
25 |
26 | - name: Copy Drupal Sonar properties files into place.
27 | copy: >
28 | src=sonar/sonar-{{ item }}.properties
29 | dest=/etc/sonar/sonar-{{ item }}.properties
30 | mode=644
31 | with_items: "{{ drupal_versions }}"
32 |
--------------------------------------------------------------------------------
/provisioning/tasks/jenkins-setup.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Check if Sonar Jenkins configuration file already exists.
3 | stat: path=/var/lib/jenkins/hudson.plugins.sonar.SonarPublisher.xml
4 | register: sonar_config_file
5 |
6 | - name: Copy Sonar Jenkins configuration to server.
7 | template: >
8 | src={{ item.src }}
9 | dest={{ item.dest }}
10 | backup=yes
11 | with_items:
12 | - src: jenkins/hudson.plugins.sonar.SonarPublisher.xml.j2
13 | dest: /var/lib/jenkins/hudson.plugins.sonar.SonarPublisher.xml
14 | - src: jenkins/hudson.plugins.sonar.SonarRunnerInstallation.xml.j2
15 | dest: /var/lib/jenkins/hudson.plugins.sonar.SonarRunnerInstallation.xml
16 | when: sonar_config_file.stat.exists == false
17 | notify: restart jenkins
18 |
19 | - name: Copy in the Drupal Jenkins job templates.
20 | copy: >
21 | src=jenkins/Drupal{{ item }}.xml
22 | dest={{ workspace }}/Drupal{{ item }}.xml
23 | with_items: "{{ drupal_versions }}"
24 | notify: restart jenkins
25 |
26 | # SEE: http://stackoverflow.com/a/9954283/100134
27 | - name: Import the Jenkins jobs.
28 | shell: >
29 | java -jar {{ jenkins_jar_location }} -s http://{{ inventory_hostname }}:8080/
30 | create-job "Drupal {{ item }}" < {{ workspace }}/Drupal{{ item }}.xml
31 | register: import
32 | changed_when: "import.stdout and 'already exists' not in import.stdout"
33 | # TODO - Also use 'or' condition for initial install... what to expect?
34 | # failed: [jenkins] => {"changed": true, "cmd": "java -jar /opt/jenkins-cli.jar -s http://jenkins:8080/ create-job Drupal7 < /root/Drupal7.xml ", "delta": "0:00:01.613581", "end": "2014-01-28 10:39:55.493576", "failed": true, "failed_when_result": true, "item": "", "rc": 0, "start": "2014-01-28 10:39:53.879995", "stdout_lines": []}
35 | failed_when: "import.stderr and 'already exists' not in import.stderr"
36 | with_items: "{{ drupal_versions }}"
37 | when: sonar_config_file.stat.exists == false
38 | notify: restart jenkins
39 |
40 | - name: Ensure Drupal Jenkins phing config directory exists.
41 | file: >
42 | path=/etc/jenkins
43 | state=directory
44 | mode=755
45 |
46 | - name: Copy Drupal Jenkins phing files into place.
47 | copy: >
48 | src=jenkins/Drupal{{ item }}-phing.xml
49 | dest=/etc/jenkins/Drupal{{ item }}-phing.xml
50 | mode=644
51 | with_items: "{{ drupal_versions }}"
52 |
--------------------------------------------------------------------------------
/provisioning/tasks/php-utils-composer.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # Use --prefer-dist so composer won't try re-downloading every update.
3 | - name: Install dependencies via Composer.
4 | command: >
5 | /usr/local/bin/composer global require {{ item }} --prefer-dist
6 | with_items: "{{ composer_global_installs }}"
7 | register: composer
8 | changed_when: "'Nothing to install or update' not in composer.stdout"
9 | environment:
10 | # Since Composer can't be run as jenkins service account, download the
11 | # dependencies to Jenkins' .composer directory instead of root's.
12 | COMPOSER_VENDOR_DIR: /var/lib/jenkins/.composer/vendor
13 |
14 | - name: Symlink binaries so Jenkins and Sonar Runner can see them easily.
15 | file: >
16 | src=/var/lib/jenkins/.composer/vendor/bin/{{ item }}
17 | dest=/usr/bin/{{ item }}
18 | state=link
19 | with_items:
20 | - pdepend
21 | - phing
22 | - phpcpd
23 | - phpcs
24 | - phpmd
25 | - phpunit
26 |
27 | - name: Change ownership of entire Jenkins .composer directory.
28 | file: >
29 | path=/var/lib/jenkins/.composer
30 | state=directory
31 | owner=jenkins
32 | group=jenkins
33 | mode=755
34 |
--------------------------------------------------------------------------------
/provisioning/tasks/sonar-php.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Install Sonar PHP plugin.
3 | get_url: >
4 | url={{ sonar_php_plugin_url }}
5 | dest=/usr/local/sonar/extensions/plugins/{{ sonar_php_plugin_jar }}
6 | notify: restart sonar
7 |
8 | - name: Wait for Sonar to start.
9 | wait_for: port=9000 delay=30
10 |
11 | - name: Check if Sonar's database is already configured correctly.
12 | shell: >
13 | mysql -u {{ sonar_mysql_user }} -p{{ sonar_mysql_password }} {{ sonar_mysql_database }} -e
14 | "SELECT COUNT(*) FROM properties WHERE text_value='Drupal'"
15 | register: sonar_setup
16 | changed_when: false
17 | failed_when: false
18 |
19 | - name: Wait for Sonar to start (again).
20 | wait_for: port=9000 delay=30
21 | when: "'0' in sonar_setup.stdout"
22 |
23 | - name: Copy Sonar database provisioning scripts to temp directory.
24 | copy: >
25 | src={{ item }}
26 | dest=/tmp/{{ item }}
27 | when: "'0' in sonar_setup.stdout"
28 | with_items:
29 | - sonar-php-custom-rules.sql
30 | - sonar-php-profile-setup.sql
31 |
32 | # Add in Drupal custom rulesets (if using v1.2 of plugin).
33 | # See: http://jira.codehaus.org/browse/SONARPHP-270 (for v2.x).
34 | - name: Set up Sonar MySQL database.
35 | shell: >
36 | mysql -u {{ sonar_mysql_user}} -p{{ sonar_mysql_password }} {{ sonar_mysql_database }}
37 | < /tmp/sonar-php-custom-rules.sql
38 | when: "'0' in sonar_setup.stdout"
39 |
40 | - name: Restart Sonar to pick up new rules.
41 | service: name=sonar state=restarted
42 | when: "'0' in sonar_setup.stdout"
43 |
44 | - name: Wait for Sonar to start (again).
45 | wait_for: port=9000 delay=30
46 | when: "'0' in sonar_setup.stdout"
47 |
48 | - name: Set up Sonar MySQL database (again).
49 | shell: >
50 | mysql -u {{ sonar_mysql_user}} -p{{ sonar_mysql_password }} {{ sonar_mysql_database }}
51 | < /tmp/sonar-php-profile-setup.sql
52 | when: "'0' in sonar_setup.stdout"
53 |
--------------------------------------------------------------------------------
/provisioning/templates/jenkins/hudson.plugins.sonar.SonarPublisher.xml.j2:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Sonar
6 | false
7 | http://{{ sonar_host }}:{{ sonar_port }}
8 |
9 | jdbc:mysql://{{ sonar_mysql_host }}:{{ sonar_mysql_port }}/{{ sonar_mysql_database }}?autoReconnect=true
10 | com.mysql.jdbc.Driver
11 | {{ sonar_mysql_user }}
12 | {{ sonar_mysql_password }}
13 |
14 |
15 | false
16 | false
17 |
18 |
19 | {{ jenkins_sonar_login }}
20 | {{ jenkins_sonar_password }}
21 |
22 |
23 |
--------------------------------------------------------------------------------
/provisioning/templates/jenkins/hudson.plugins.sonar.SonarRunnerInstallation.xml.j2:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Sonar Runner
6 | /usr/local/sonar-runner
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/provisioning/vars/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | ntp_timezone: America/Chicago
3 | ntp_enabled: false
4 | workspace: /root
5 |
6 | # Each version supported requires DrupalX.xml and sonar-x.properties files.
7 | drupal_versions:
8 | - "7"
9 | - "8"
10 |
11 | # Firewall configuration.
12 | firewall_allowed_tcp_ports:
13 | - "22"
14 | - "25"
15 | - "80"
16 | - "443"
17 | - "8080"
18 | - "9000"
19 | firewall_forwarded_tcp_ports:
20 | - { src: "80", dest: "9000" }
21 | firewall_additional_rules:
22 | - "iptables -A INPUT -p tcp --dport 4949 -s 167.88.120.81 -j ACCEPT"
23 |
24 | # Munin configuration.
25 | munin_node_host_name: "{{ inventory_hostname }}"
26 | munin_node_allowed_ips:
27 | - '^167\.88\.120\.81$'
28 |
29 | # Java configuration.
30 | java_packages:
31 | - java-1.7.0-openjdk
32 |
33 | # Jenkins configuration.
34 | jenkins_jar_location: /opt/jenkins-cli.jar
35 | jenkins_plugins:
36 | - phing
37 | - git
38 | - sonar
39 | - ssh
40 |
41 | # Sonar configuration.
42 | # sonar_host: localhost (set in inventory)
43 | sonar_port: 9000
44 | # sonar_mysql_host: "{{ sonar_host }}" (set in inventory)
45 | sonar_mysql_port: 3306
46 |
47 | sonar_download_url: http://downloads.sonarsource.com/sonarqube/sonar-3.7.4.zip
48 | sonar_version_directory: sonar-3.7.4
49 | #sonar_mysql_allowed_hosts: [] (set in host_vars)
50 |
51 | sonar_php_plugin_url: http://downloads.sonarsource.com/plugins/org/codehaus/sonar-plugins/php/sonar-php-plugin/1.2/sonar-php-plugin-1.2.jar
52 | sonar_php_plugin_jar: sonar-php-plugin-1.2.jar
53 |
54 | # PHP configuration.
55 | php_enablerepo: remi
56 | php_packages:
57 | - php
58 | - php-devel
59 | - php-xml
60 | php_memory_limit: "512M"
61 | php_max_execution_time: "120"
62 | # Hack to stop restarting Apache.
63 | php_webserver_daemon: ntpd
64 |
65 | # Composer configuration.
66 | composer_global_packages:
67 | - { name: hirak/prestissimo, release: '^0.3' }
68 | composer_global_installs:
69 | - pdepend/pdepend=~1.1
70 | - phpunit/phpunit=3.7.32
71 | - phpmd/phpmd=1.5.0
72 | - sebastian/phpcpd=2.0.0
73 | - squizlabs/php_codesniffer=1.5.3
74 | - phing/phing=2.7.0
75 |
76 | # MySQL configuration.
77 | mysql_max_allowed_packet: "32M"
78 |
--------------------------------------------------------------------------------
/requirements.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - src: geerlingguy.firewall
3 | - src: geerlingguy.repo-epel
4 | - src: geerlingguy.repo-remi
5 | - src: geerlingguy.ntp
6 | - src: geerlingguy.munin-node
7 | - src: geerlingguy.git
8 | - src: geerlingguy.java
9 | - src: geerlingguy.jenkins
10 | - src: geerlingguy.php
11 | - src: geerlingguy.composer
12 | - src: geerlingguy.sonar-runner
13 | - src: geerlingguy.sonar
14 |
--------------------------------------------------------------------------------
/screenshot.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/geerlingguy/drupalci-sonar-jenkins/9e8a9b36ce31fb13c7f65c2477fb0fbb4e359887/screenshot.jpg
--------------------------------------------------------------------------------