├── .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 | Drupal CI SonarQube Dashboard 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 --------------------------------------------------------------------------------