├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── composer.json ├── docs ├── .nojekyll ├── README.md ├── assets │ ├── forge_deployment_webhook.png │ ├── laravel-sanity-banner-v2.png │ └── laravel-sanity-banner.png └── index.html ├── phpunit.xml ├── src ├── Badges.php ├── Commands │ ├── SanityMock.php │ └── SanityReset.php ├── Config │ ├── config.php │ └── phpcs.xml ├── Events │ └── RunnerEvent.php ├── Factory.php ├── Routes │ └── routes.php ├── Runners │ ├── DuskTestRunner.php │ ├── Runner.php │ ├── RunnerForMiniGame.php │ ├── ScoreboardRunner.php │ ├── StyleTestRunner.php │ └── UnitTestRunner.php ├── SanityServiceProvider.php ├── SlackMessage.php └── Subscriber.php └── tests ├── .gitkeep ├── TestCase.php └── Unit └── SanityTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: true 4 | 5 | php: 6 | - 7.2 7 | 8 | services: 9 | - sqlite 10 | 11 | before_script: 12 | - composer install 13 | - travis_retry composer self-update 14 | - travis_retry composer update --no-interaction --prefer-dist 15 | - composer show laravel/framework 16 | - composer dumpautoload 17 | 18 | script: 19 | - vendor/bin/phpunit 20 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Stephen Lake 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Sanity 2 | 3 | ![tests](https://img.shields.io/travis/stephenlake/laravel-sanity/master.svg?style=flat-square) 4 | ![scrutinzer](https://img.shields.io/scrutinizer/g/stephenlake/laravel-sanity.svg?style=flat-square) 5 | ![downloads](https://img.shields.io/packagist/dt/stephenlake/laravel-sanity.svg?style=flat-square) 6 | ![release](https://img.shields.io/github/release/stephenlake/laravel-sanity.svg?style=flat-square) 7 | ![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square) 8 | 9 | **Laravel Sanity** is a **staging** package to assist in running your own unit testing, code standards and custom runner automation on your staging/testing environment using any deployment service that allows post-deployment webhook triggers. Ideal for start-ups or teams that prefer a simple testing environment over enterprise CI. Keep track of breakage scores and compete with your team in keeping your codebase clean! 10 | 11 | Made with ❤️ by [Stephen Lake](http://github.com/stephenlake) 12 | 13 | ## Getting Started 14 | 15 | Install the package via composer. 16 | 17 | composer require cloudcake/laravel-sanity 18 | 19 | #### See [documentation](https://stephenlake.github.io/laravel-sanity/) for full installation and getting started. 20 | 21 | ## License 22 | 23 | This library is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudcake/laravel-sanity", 3 | "description": "Automate builds and code standards sniffing. Now with badges.", 4 | "keywords": ["automate", "ci", "testing", "badges"], 5 | "license": "MIT", 6 | "type": "library", 7 | "authors": [{ 8 | "name": "Stephen Lake", 9 | "email": "stephen@closurecode.com" 10 | }], 11 | "require": { 12 | "phpunit/phpunit": "~7.0|~8.0", 13 | "squizlabs/php_codesniffer": "^3.4", 14 | "kitetail/zttp": "^0.4.0" 15 | }, 16 | "require-dev": { 17 | "laravel/framework": "~5.5.0|~5.6.0|~5.7.0|~5.8.0", 18 | "orchestra/testbench": "~3.4.0|~3.5.0|~3.6.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Sanity\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Sanity\\Tests\\": "tests/" 28 | } 29 | }, 30 | "extra": { 31 | "laravel": { 32 | "providers": [ 33 | "Sanity\\SanityServiceProvider" 34 | ] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcake/laravel-sanity/5b60c67e206ded0f82343070918beb615d1d08e6/docs/.nojekyll -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | Self hosted in-app testing, coding standards and custom runner automation for Laravel. Now with badges! 7 |
8 | 9 | # Getting Started 10 | 11 | ## Install the package via composer 12 | 13 | composer require stephenlake/laravel-sanity --dev 14 | 15 | ## Register the service provider 16 | 17 | This package makes use of Laravel's auto-discovery. If you are an using earlier version of Laravel (< 5.4) you will need to manually register the service provider: 18 | 19 | Add `Sanity\SanityServiceProvider::class` to the `providers` array in `config/app.php`. 20 | 21 | ## Publish configuration 22 | 23 | `php artisan vendor:publish --provider="Sanity\SanityServiceProvider" --tag="config"` 24 | 25 | This will publish `sanity.php` to your `config/` path and a `phpcs.xml` config in your base app path. 26 | 27 | ## Install Sanity's Routes 28 | 29 | Generally packages would load their routes automatically, however Sanity allows you to customise your routes in your `config/sanity.php` config and wrap them in groups if you like. 30 | 31 | Open up your routes file (default: `routes/web.php`) and add: 32 | `\Sanity\Factory::routes();` 33 | 34 | This will load the configured routes (in `config/sanity.php`). 35 | 36 | ## Configure Your Deployment Service 37 | 38 | It's important to keep in mind that this package is intended to be run on your staging/testing servers. While you may run it on production, it's highly discouraged as tests are run **after** the code has already been deployed. 39 | 40 | The Sanity webhook is the URL that must be hit to trigger Sanity to process its tests. Enter your full domain and the configured webhook endpoint into your deployment configuration: 41 | 42 | **Laravel Forge Example:** 43 | 44 | ![forge_deployment_webhook.png](https://github.com/stephenlake/laravel-sanity/raw/master/docs/assets/forge_deployment_webhook.png) 45 | 46 | Where `example.org` is your applications **staging/testing** domain name and `/sanity/forge` is the endpoint you have configured in `config/sanity.php` under the `webhook` field. 47 | 48 | # Usage 49 | 50 | ## Runners 51 | 52 | Runners are individual classes used to perform tests (and other tasks) on your code base and store the result with their state at the time of the latest commit. Runners store information like the committer, the state before and after the committer triggered the runner and whether they broke or fixed your code. 53 | 54 | ### Prepackaged Runners 55 | 56 | Sanity is packaged with some useful predefined runners out of the box to express its awesomeness. 57 | 58 | #### Unit Runner 59 | 60 | [(`\Sanity\Runners\UnitTestRunner`)](https://github.com/stephenlake/laravel-sanity/blob/master/src/Runners/UnitTestRunner.php) 61 | 62 | The unit runner runs your configured PHPUnit tests. 63 | 64 | #### Dusk Runner 65 | 66 | [(`\Sanity\Runners\DuskTestRunner`)](https://github.com/stephenlake/laravel-sanity/blob/master/src/Runners/DuskTestRunner.php) 67 | 68 | The dusk runner runs your configured Laravel Dusk tests. 69 | 70 | #### Style Runner 71 | 72 | [(`\Sanity\Runners\StyleTestRunner`)](https://github.com/stephenlake/laravel-sanity/blob/master/src/Runners/StyleTestRunner.php) 73 | 74 | The style runner performs a strict set of PSR (with some customizations) rules to ensure that your code format and documentation is top-notch. 75 | 76 | #### Scoreboard Runner 77 | 78 | [(`\Sanity\Runners\ScoreboardRunner`)](https://github.com/stephenlake/laravel-sanity/blob/master/src/Runners/ScoreboardRunner.php) 79 | 80 | The scoreboard runner applies a points system to each pusher. When a pusher fixes, breaks or leaves other runners in the same state they were before their push, an allocated number of points (could be negative) will be associated to that user. 81 | 82 | 83 | ### Creating Custom Runners 84 | 85 | Creating your own runners couldn't be easier. Simply create a class that extends the Sanity base runner, give it a name and provide a run method that ends with telling Sanity whether your runner succeeded or failed. Let's run through an example. 86 | 87 | We'll create a runner that tests if the `storage` directory is writable. 88 | 89 | #### Create your runner class 90 | 91 | markAsPassed(); 105 | } else { 106 | $this->markAsFailed(); 107 | } 108 | } 109 | } 110 | 111 | #### Add your runner to the config 112 | 113 | 'runners' => [ 114 | Sanity\Runners\UnitTestRunner::class, 115 | Sanity\Runners\DuskTestRunner::class, 116 | Sanity\Runners\StyleTestRunner::class, 117 | Sanity\Runners\ScoreboardRunner::class, 118 | App\WritableStorageTestRunner::class 119 | ], 120 | 121 | #### Test your runner 122 | 123 | Sanity comes bundled with a test command (`\Sanity\Commands\SanityMock`) which may be used to simulate a deployment payload from Laravel Forge. Run this command to test your new runner: 124 | 125 | `php artisan sanity:mock` 126 | 127 | It's that simple. And you magically have a badge to display, check it out by opening your configured badges endpoint, defaults to `http://localhost/sanity/badges/writable-storage.svg`! 128 | 129 | ![badge](https://img.shields.io/badge/Storage%20Test-passing-99cc00.svg) 130 | 131 | ### Available Attributes Options 132 | 133 | The base Sanity runner class contains a number of proctected attributes to customize your runners. See the complete list below: 134 | 135 | #### `name` 136 | 137 | `protected $name` `string` default: `Runner` 138 | 139 | A unique name for the runner. Must be changed. Used for mapping runners and must be unique. 140 | 141 | #### `badgeLabel` 142 | 143 | `protected $badgeLabel` `string` default: `Runner` 144 | 145 | The label to display on the generated badge. 146 | 147 | #### `badgeColourPassing` 148 | 149 | `protected $badgeColourPassing` `string` default: `99cc00` 150 | 151 | The colour of the badge when passing. The value should be a hex value **without the leading hash (#)**. 152 | 153 | #### `badgeColourFailing` 154 | 155 | `protected $badgeColourFailing` `string` default: `c53232` 156 | 157 | The colour of the badge when failing. The value should be a hex value **without the leading hash (#)**. 158 | 159 | #### `badgeColourUnknown` 160 | 161 | `protected $badgeColourUnknown` `string` default: `989898` 162 | 163 | The colour of the badge when pending. The value should be a hex value **without the leading hash (#)**. 164 | 165 | #### `badgeValuePassing` 166 | 167 | `protected $badgeValuePassing` `string` default: `passing` 168 | 169 | The text to display when the runner is passing. 170 | 171 | #### `badgeValueFailing` 172 | 173 | `protected $badgeValueFailing` `string` default: `failing` 174 | 175 | The text to display when the runner is failing. 176 | 177 | #### `badgeValueUnknown` 178 | 179 | `protected $badgeValueUnknown` `string` default: `pending` 180 | 181 | The text to display when the runner hasn't run or is pending. 182 | 183 | #### `shouldFireEvents` 184 | 185 | `protected $shouldFireEvents` `boolean` default: `true` 186 | 187 | Boolean value indicated whether or not the runner should fire success and failure events once it has been run. 188 | 189 | #### `collectsStats` 190 | 191 | `protected $collectsStats` `boolean` default: `false` 192 | 193 | If set to true, the runner will run after all other runners that are not set to collect stats. This is useful when you need your runner to be executed after everything else has been run in order to collect the results of the other runners. 194 | 195 | ### Available Runner Methods 196 | 197 | Runners' helper methods: 198 | 199 | #### `markAsPassed()` 200 | 201 | Mark the runner as a success. 202 | 203 | #### `markAsFailed()` 204 | 205 | Mark the runner as a failure. 206 | 207 | #### `passing()` 208 | 209 | Returns true if the runner has passed. 210 | 211 | #### `failing()` 212 | 213 | Returns true if the runner has failed. 214 | 215 | #### `isCurrentlyPassing()` 216 | 217 | Alias of `passing()`. Returns true if the runner has passed. 218 | 219 | #### `isCurrentlyFailing()` 220 | 221 | Alias of `failing()`. Returns true if the runner has failed. 222 | 223 | #### `hasntRun()` 224 | 225 | Returns true if the runner hasn't run yet. 226 | 227 | #### `setResults(array $results)` 228 | 229 | Stores logs in array format for the recorded runner. This is required if you wish to preset logs in your notifications. 230 | 231 | #### `getCommit()` 232 | 233 | Get the latest commit information from the push that triggered the runner to execute. 234 | 235 | #### `getResults()` 236 | 237 | Get stored logs from the runner as an array. 238 | 239 | #### `wasJustBroken()` 240 | 241 | Returns true if this runner was previously successful, but currently failing. 242 | 243 | #### `wasJustFixed()` 244 | 245 | Returns true if this runner was previously failing, but currently passing. 246 | 247 | #### `collectsStats()` 248 | 249 | Returns true if the runner collects stats. 250 | 251 | ## Listening for results 252 | 253 | The configuration file contains a `subscriber` field which if undefined points to a default subscriber that listens for events. If you would like your app to listen to these events and fire off your own notifications, you may do so by creating your own subscriber class and extending `Sanity\Subscriber`. 254 | 255 | ### Create your subscriber class 256 | 257 | Create a `SanityEventSubscriber.php` file in `app/` with content: 258 | 259 | `App\SanityEventSubscriber::class` 302 | 303 | ## Slack notifications 304 | 305 | The base subscriber is bundled with a little Slack helper to assist in submitting Slack notifications via webhook URL. 306 | 307 | getCommit(); 320 | $pusher = $commit['commit_author']; 321 | 322 | if ($runner->wasJustFixed()) { 323 | slack(self::MY_SLACK_WEHBOOK_URL) 324 | ->success() 325 | ->title("{$pusher} fixed the tests!") 326 | ->text("{$pusher} committed and fixed broken tests!") 327 | ->send(); 328 | } else { 329 | slack(self::MY_SLACK_WEHBOOK_URL) 330 | ->success() 331 | ->title("The tests are still passing!") 332 | ->text("{$pusher} committed without breaking the test!") 333 | ->send(); 334 | } 335 | } 336 | 337 | public function onMyRunnerFailure($runner) 338 | { 339 | if ($runner->wasJustBroken()) { 340 | slack(self::MY_SLACK_WEHBOOK_URL) 341 | ->danger() 342 | ->title("{$pusher} broke the tests!") 343 | ->text("{$pusher} committed and broke a passing test!") 344 | ->send(); 345 | } else { 346 | slack(self::MY_SLACK_WEHBOOK_URL) 347 | ->danger() 348 | ->title("The tests are still broken!") 349 | ->text("{$pusher} committed without fixing the test!") 350 | ->send(); 351 | } 352 | } 353 | } 354 | 355 | If the provider slack helpers are insufficient for your needs, you may submit raw payloads using the `raw()` method. 356 | 357 | # Extended Usage 358 | 359 | ## Adding pre-runners 360 | 361 | There may be situations where you need to run some setup before the tests commence. You may define pre-runner classes in the config within the `pre-runners` block. These classes must be instatiable and contain a public `run` method, example: 362 | 363 | Add the `\App\MyExamplePreRunner::class` file to the `pre-runners` block in `configs/sanity.php`: 364 | 365 | 'pre-runners' => [ 366 | \App\MyExamplePreRunner::class, 367 | ], 368 | 369 | Create the pre-runner: 370 | 371 | [ 398 | \App\MyExamplePostRunner::class, 399 | ], 400 | 401 | Create the pre-runner: 402 | 403 | 2 | 3 | 4 | 5 | Laravel Sanity 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/Unit/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Badges.php: -------------------------------------------------------------------------------- 1 | cache = Cache::store(config('sanity.cache'), 'file'); 27 | } 28 | 29 | /** 30 | * Get badge for runner instance. 31 | * 32 | * @return void 33 | */ 34 | public function get($runner, $queryString) 35 | { 36 | $label = $runner->getBadgeLabel(); 37 | $status = $runner->getBadgeStatus(); 38 | $colour = $runner->getBadgeColour(); 39 | 40 | $badge = $this->getBadge($label, $status, $colour, $queryString); 41 | 42 | if ($badge) { 43 | return response($badge, 200)->header('Content-Type', 'image/svg+xml'); 44 | } 45 | 46 | return abort(404); 47 | } 48 | 49 | /** 50 | * Get badge from cache otherwise create new. 51 | * 52 | * @return string 53 | */ 54 | private function getBadge($label, $status, $colour, $queryString) 55 | { 56 | $keymd = md5($label.$status.$colour.$queryString); 57 | $badge = $this->cache->get("sanity.badges.{$keymd}", false); 58 | 59 | if (!$badge) { 60 | $badge = $this->getNewBadge($label, $status, $colour, $queryString); 61 | } 62 | 63 | return $badge; 64 | } 65 | 66 | /** 67 | * Fetch badge from shields.io. 68 | * 69 | * @return string|bool 70 | */ 71 | private function getNewBadge($label, $status, $colour, $queryString) 72 | { 73 | $url = sprintf(self::BADGE_URL, $label, $status, $colour, $queryString); 74 | 75 | $res = Zttp::get($url); 76 | 77 | if ($res->isOk()) { 78 | $badge = $res->body(); 79 | $keymd = md5($label.$status.$colour.$queryString); 80 | $this->cache->forever("sanity.badges.{$keymd}", $badge); 81 | 82 | return $badge; 83 | } 84 | 85 | return false; 86 | } 87 | 88 | /** 89 | * Get the actual badge and cache it. 90 | * 91 | * @return \Illuminate\Routing\ResponseFactory 92 | */ 93 | private function badgeResponse($label, $status, $colour) 94 | { 95 | $opt = request()->getQueryString(); 96 | $url = "https://img.shields.io/badge/{$label}-{$status}-{$colour}.svg?{$opt}"; 97 | $md5 = md5($url); 98 | $key = "sanity.badges.{$md5}"; 99 | 100 | if ($this->cache->has($key)) { 101 | $badge = $this->cache->get($key); 102 | } else { 103 | $badge = file_get_contents($url); 104 | $this->cache->forever($key, $badge); 105 | } 106 | 107 | return response($badge, 200)->header('Content-Type', 'image/svg+xml'); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Commands/SanityMock.php: -------------------------------------------------------------------------------- 1 | forget("sanity.{$runner->getKeyName()}"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Config/config.php: -------------------------------------------------------------------------------- 1 | ['local', 'testing'], 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Webhook Route Endpoint 27 | |-------------------------------------------------------------------------- 28 | | 29 | | You may define your own webhook endpoint here. This is the endpoint that 30 | | mustbe hit to trigger Sanity to execute all runners. 31 | | 32 | | It's recommended that you make this URL somewhat secretive by making it 33 | | a little more verbose than it currently is (add some long hash) to prevent 34 | | unauthorised hits. 35 | | 36 | */ 37 | 38 | 'webhook' => '/sanity/build', 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Badge Route Endpoint 43 | |-------------------------------------------------------------------------- 44 | | 45 | | You can define your desired badge endpoint route if the default does not 46 | | suit your needs. This is the endpoint that must be hit to produce an image 47 | | badge for the given runner. 48 | | 49 | | If changing the route, you must include the {runner} placeholder which will 50 | | identify which badge to fetch. 51 | | 52 | */ 53 | 54 | 'badges' => '/sanity/badges/{runner}.svg', 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Results Route Endpoint 59 | |-------------------------------------------------------------------------- 60 | | 61 | | You can define your desired results endpoint route if the default does not 62 | | suit your needs. This is the endpoint that provides output results of each 63 | | runner. 64 | | 65 | */ 66 | 67 | 'results' => '/sanity/results/{runner}', 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Enabled Runners 72 | |-------------------------------------------------------------------------- 73 | | 74 | | Define which runners should be run with a boolean value. Any badges that 75 | | are called for disabled runners will return as 'not running' or the last 76 | | known state before the runner was disabled. 77 | | 78 | | See documentation for descriptions of the predefined runners. 79 | | 80 | */ 81 | 82 | 'runners' => [ 83 | Sanity\Runners\UnitTestRunner::class, 84 | Sanity\Runners\DuskTestRunner::class, 85 | Sanity\Runners\StyleTestRunner::class, 86 | Sanity\Runners\ScoreboardRunner::class, 87 | ], 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Subscriber 92 | |-------------------------------------------------------------------------- 93 | | 94 | | By default Sanity will catch its own events. Replacing the below subscriber 95 | | with your own which extends the Sanity subsriber will allow your app to 96 | | catch these all events events and handle them as you wish, perhaps with 97 | | email and/or slack notifications. 98 | | 99 | | See the Sanity documentation for the list of events to cater for: 100 | | https://github.com/stephenlake/laravel-sanity 101 | | 102 | */ 103 | 104 | 'subscriber' => Sanity\Subscriber::class, 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Cache 109 | |-------------------------------------------------------------------------- 110 | | 111 | | The desired cache store to use when storing information on your app's 112 | | current state, ie: test results, coding standard results and commit info. 113 | | 114 | | This store must align with what's configured in your config/cache.php 115 | | configurations. 116 | | 117 | */ 118 | 119 | 'cache' => 'file', 120 | 121 | /* 122 | |-------------------------------------------------------------------------- 123 | | Pre-runners 124 | |-------------------------------------------------------------------------- 125 | | 126 | | If there's tasks you wish to run before Sanity runs its checks, you can 127 | | define your pre-runner classes here. Each class requires a public run() 128 | | method which will be called. 129 | | 130 | */ 131 | 132 | 'pre-runners' => [], 133 | 134 | /* 135 | |-------------------------------------------------------------------------- 136 | | Post-runners 137 | |-------------------------------------------------------------------------- 138 | | 139 | | If there's tasks you wish to run after Sanity runs its checks, you can 140 | | define your post-runner classes here. Each class requires a public run() 141 | | method which will be called. 142 | | 143 | */ 144 | 145 | 'post-runners' => [], 146 | 147 | ]; 148 | -------------------------------------------------------------------------------- /src/Config/phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PSR2 coding standards with minor tweaks 4 | 5 | app 6 | config 7 | public 8 | resources 9 | routes 10 | tests 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | *.php 78 | 79 | 80 | *.php 81 | 82 | 83 | 84 | 85 | */database/* 86 | */cache/* 87 | */*.js 88 | */*.css 89 | */*.xml 90 | */*.blade.php 91 | */autoload.php 92 | */storage/* 93 | */docs/* 94 | */vendor/* 95 | */migrations/* 96 | */config/* 97 | */public/index.php 98 | */*.blade.php 99 | */Middleware/* 100 | */Console/Kernel.php 101 | */Exceptions/Handler.php 102 | */Http/Kernel.php 103 | */Providers/* 104 | 105 | -------------------------------------------------------------------------------- /src/Events/RunnerEvent.php: -------------------------------------------------------------------------------- 1 | runner = $runner; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | setupRunners(); 24 | } 25 | 26 | /** 27 | * Get package routes. 28 | * 29 | * @return mixed 30 | */ 31 | public static function routes() 32 | { 33 | require __DIR__.'/Routes/routes.php'; 34 | } 35 | 36 | /** 37 | * Create instantiations of valid runners. 38 | * 39 | * @throws Exception If a duplicate runner name is found. 40 | * 41 | * @return void 42 | */ 43 | private function setupRunners() 44 | { 45 | $runners = config('sanity.runners', []); 46 | 47 | foreach ($runners as $runner) { 48 | if (is_subclass_of($runner, \Sanity\Runners\Runner::class)) { 49 | $runnerInstance = new $runner(); 50 | $runnerKey = $runnerInstance->getKeyName(); 51 | 52 | if (isset(self::$runners[$runnerKey])) { 53 | throw new \Exception("Duplicate runner name defined: {$runnerInstance->getName()}"); 54 | } 55 | 56 | self::$runners[$runnerKey] = $runnerInstance; 57 | } 58 | } 59 | } 60 | 61 | /** 62 | * Run all configured tasks. 63 | * 64 | * @return void 65 | */ 66 | public function runRunners($commit) 67 | { 68 | $this->checkEnvironment(); 69 | $this->runPreRunners(); 70 | 71 | // Stat collectors must run last 72 | self::$runners = collect(self::$runners)->sortBy(function ($runner, $s) { 73 | return $runner->collectsStats(); 74 | })->all(); 75 | 76 | foreach (self::$runners as $runner) { 77 | $runner->runNow($commit); 78 | } 79 | 80 | $this->runPostRunners(); 81 | } 82 | 83 | /** 84 | * Tasks to run before running tests. 85 | * 86 | * @return void 87 | */ 88 | public function runPreRunners() 89 | { 90 | $preRunners = config('sanity.pre-runners', []); 91 | 92 | if (count($preRunners)) { 93 | foreach ($preRunners as $runner) { 94 | if (class_exists($runner)) { 95 | (new $runner())->run($this->deployment); 96 | } 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * Tasks to run after running tests. 103 | * 104 | * @return void 105 | */ 106 | public function runPostRunners() 107 | { 108 | $postRunners = config('sanity.post-runners', []); 109 | 110 | if (count($postRunners)) { 111 | foreach ($postRunners as $runner) { 112 | if (class_exists($runner)) { 113 | (new $runner())->run($this->deployment); 114 | } 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Make sure we're running this on an allowed environment. 121 | * 122 | * @return void 123 | */ 124 | private function checkEnvironment() 125 | { 126 | $environmentCurrent = env('APP_ENV', 'production'); 127 | $environmentsAllowed = config('environments', ['local', 'testing']); 128 | 129 | if (!in_array($environmentCurrent, $environmentsAllowed, true)) { 130 | exit; 131 | } 132 | } 133 | 134 | /** 135 | * Get runner badge. 136 | * 137 | * @return \Illuminate\Routing\ResponseFactory 138 | */ 139 | public function badge($runnerName, $queryString = '') 140 | { 141 | $runner = self::$runners[Str::slug($runnerName)] ?? false; 142 | 143 | if ($runner) { 144 | return \Facades\Sanity\Badges::get($runner, $queryString); 145 | } 146 | 147 | return abort(404); 148 | } 149 | 150 | /** 151 | * Get runner results. 152 | * 153 | * @param string $runnerName Name of the runner. 154 | * 155 | * @return \Illuminate\Routing\ResponseFactory 156 | */ 157 | public function results($runnerName) 158 | { 159 | if (auth()->user()) { 160 | $result = false; 161 | $runner = self::$runners[Str::slug($runnerName)] ?? false; 162 | 163 | if ($runner) { 164 | $result = $this->formatResult($runner->getResults()); 165 | } 166 | 167 | if ($result) { 168 | return $result; 169 | } 170 | } 171 | 172 | return abort(404); 173 | } 174 | 175 | /** 176 | * Format runner results. 177 | * 178 | * @param array $result The result array to format. 179 | * 180 | * @return array 181 | */ 182 | private function formatResult($result) 183 | { 184 | if (($format = strtoupper(request()->query('format', 'array')))) { 185 | switch ($format) { 186 | case 'JSON': 187 | $result = json_encode($result); 188 | break; 189 | } 190 | } 191 | 192 | return $result; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Routes/routes.php: -------------------------------------------------------------------------------- 1 | all()); 5 | }); 6 | 7 | Route::get(config('sanity.badges', '/sanity/badges/{runner}.svg'), function ($runner) { 8 | return Facades\Sanity\Factory::badge($runner, request()->getQueryString()); 9 | }); 10 | 11 | Route::get(config('sanity.results', '/sanity/results/{runner}'), function ($runner) { 12 | return Facades\Sanity\Factory::results($runner); 13 | }); 14 | -------------------------------------------------------------------------------- /src/Runners/DuskTestRunner.php: -------------------------------------------------------------------------------- 1 | setResults($results); 41 | 42 | ($code == 0) ? $this->markAsPassed() : $this->markAsFailed(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Runners/Runner.php: -------------------------------------------------------------------------------- 1 | cache = Cache::store(config('sanity.cache'), 'file'); 116 | $this->store = $this->cache->get("sanity.{$this->getKeyName()}", [ 117 | 'state' => -1, 118 | 'previousPassing' => false, 119 | 'previousFailing' => false, 120 | 'previousCommit' => false, 121 | 'results' => [], 122 | ]); 123 | } 124 | 125 | /** 126 | * Execute runner. 127 | * 128 | * @return self 129 | */ 130 | public function runNow(array $commit) 131 | { 132 | $this->commit = $commit; 133 | 134 | $this->run(); 135 | 136 | if ($this->collectsStats()) { 137 | $this->markAsPassed(); 138 | } 139 | 140 | $this->fireEvents(); 141 | } 142 | 143 | /** 144 | * Runner execution. 145 | * 146 | * @return void 147 | */ 148 | protected function run() : void 149 | { 150 | // Subclass execution. 151 | } 152 | 153 | public function getName() : string 154 | { 155 | return $this->name; 156 | } 157 | 158 | /** 159 | * Set runner as passing. 160 | * 161 | * @return self 162 | */ 163 | public function markAsPassed() 164 | { 165 | if ($this->isCurrentlyFailing()) { 166 | $this->store['previousFailing'] = true; 167 | $this->store['previousPassing'] = false; 168 | } else { 169 | $this->store['previousFailing'] = false; 170 | $this->store['previousPassing'] = true; 171 | } 172 | 173 | $this->store['state'] = 1; 174 | 175 | $this->cacheState(); 176 | 177 | return $this; 178 | } 179 | 180 | /** 181 | * Set runner as failed. 182 | * 183 | * @return self 184 | */ 185 | public function markAsFailed() 186 | { 187 | if ($this->isCurrentlyPassing()) { 188 | $this->store['previousFailing'] = false; 189 | $this->store['previousPassing'] = true; 190 | } else { 191 | $this->store['previousFailing'] = true; 192 | $this->store['previousPassing'] = false; 193 | } 194 | 195 | $this->store['state'] = 0; 196 | 197 | $this->cacheState(); 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * Return true if runner was previously failing. 204 | * 205 | * @return bool 206 | */ 207 | public function wasPreviouslyFailing() 208 | { 209 | return $this->store['previousFailing']; 210 | } 211 | 212 | /** 213 | * Return true if runner was previously passing. 214 | * 215 | * @return bool 216 | */ 217 | public function wasPreviouslyPassing() 218 | { 219 | return $this->store['previousPassing']; 220 | } 221 | 222 | /** 223 | * Return true if runner is currently passing. 224 | * 225 | * @return bool 226 | */ 227 | public function isCurrentlyPassing() 228 | { 229 | return $this->passing(); 230 | } 231 | 232 | /** 233 | * Return true if runner is currently passing. 234 | * 235 | * @return bool 236 | */ 237 | public function isCurrentlyFailing() 238 | { 239 | return $this->failing(); 240 | } 241 | 242 | /** 243 | * Return true if runner was previously failing and is now passing. 244 | * 245 | * @return bool 246 | */ 247 | public function wasJustFixed() 248 | { 249 | return $this->wasPreviouslyFailing() && $this->isCurrentlyPassing(); 250 | } 251 | 252 | /** 253 | * Return true if runner was previously failing and is now passing. 254 | * 255 | * @return bool 256 | */ 257 | public function wasJustBroken() 258 | { 259 | return $this->wasPreviouslyPassing() && $this->isCurrentlyFailing(); 260 | } 261 | 262 | /** 263 | * Return true if runner is passing. 264 | * 265 | * @return bool 266 | */ 267 | public function passing() 268 | { 269 | return $this->store['state'] == 1; 270 | } 271 | 272 | /** 273 | * Return true if runner is failing. 274 | * 275 | * @return bool 276 | */ 277 | public function failing() 278 | { 279 | return $this->store['state'] == 0; 280 | } 281 | 282 | /** 283 | * Return true if runner has not run. 284 | * 285 | * @return bool 286 | */ 287 | public function hasntRun() 288 | { 289 | return $this->store['state'] == -1; 290 | } 291 | 292 | /** 293 | * Set output results. 294 | * 295 | * @return self 296 | */ 297 | public function setResults(array $results) 298 | { 299 | $this->store['results'] = $results; 300 | 301 | $this->cacheState(); 302 | 303 | return $this; 304 | } 305 | 306 | /** 307 | * Get output results. 308 | * 309 | * @return array 310 | */ 311 | public function getResults() 312 | { 313 | return $this->store['results']; 314 | } 315 | 316 | /** 317 | * Get the current commit information. 318 | * 319 | * @return array 320 | */ 321 | public function getCommit() 322 | { 323 | if (!$this->commit) { 324 | exec('git log -1 --pretty=format:\''. 325 | '{"commit": "%H",'. 326 | '"abbreviated_commit": "%h",'. 327 | '"tree": "%T",'. 328 | '"abbreviated_tree": "%t",'. 329 | '"parent": "%P",'. 330 | '"abbreviated_parent": "%p",'. 331 | '"refs": "%D",'. 332 | '"encoding": "%e",'. 333 | '"subject": "%s",'. 334 | '"sanitized_subject_line": "%f",'. 335 | '"body": "%b",'. 336 | '"commit_notes": "",'. 337 | '"verification_flag": "%G?",'. 338 | '"signer": "%GS",'. 339 | '"signer_key": "%GK",'. 340 | '"author": {'. 341 | '"name": "%aN",'. 342 | '"email": "%aE",'. 343 | '"date": "%aD"},'. 344 | '"commiter": {"'. 345 | 'name": "%cN",'. 346 | '"email": "%cE",'. 347 | '"date": "%cD"'. 348 | '}'. 349 | '}\'', $output); 350 | 351 | $this->commit = json_decode($output[0]); 352 | } 353 | 354 | return $this->commit; 355 | } 356 | 357 | /** 358 | * Get key name. 359 | * 360 | * @return string 361 | */ 362 | public function getKeyName() 363 | { 364 | if (!$this->keyName) { 365 | $this->keyName = Str::slug($this->name); 366 | } 367 | 368 | return $this->keyName; 369 | } 370 | 371 | /** 372 | * Get badge label. 373 | * 374 | * @return string 375 | */ 376 | public function getBadgeColour() 377 | { 378 | if ($this->passing()) { 379 | return $this->badgeColourPassing; 380 | } 381 | 382 | if ($this->failing()) { 383 | return $this->badgeColourFailing; 384 | } 385 | 386 | return $this->badgeColourUnknown; 387 | } 388 | 389 | /** 390 | * Get badge label. 391 | * 392 | * @return string 393 | */ 394 | public function getBadgeLabel() 395 | { 396 | return rawurlencode($this->badgeLabel); 397 | } 398 | 399 | /** 400 | * Get badge status. 401 | * 402 | * @return string 403 | */ 404 | public function getBadgeStatus() 405 | { 406 | $status = $this->badgeValueUnknown; 407 | 408 | if ($this->passing()) { 409 | $status = $this->badgeValuePassing; 410 | } elseif ($this->failing()) { 411 | $status = $this->badgeValueFailing; 412 | } 413 | 414 | return rawurlencode($status); 415 | } 416 | 417 | /** 418 | * Return true if this runner collects stats. 419 | * 420 | * @return bool 421 | */ 422 | public function collectsStats() 423 | { 424 | return $this->collectsStats; 425 | } 426 | 427 | /** 428 | * Fire runner events, if configured. 429 | * 430 | * @return self 431 | */ 432 | private function fireEvents() 433 | { 434 | if ($this->shouldFireEvents) { 435 | event(new \Sanity\Events\RunnerEvent($this)); 436 | } 437 | } 438 | 439 | /** 440 | * Store the current state of the runner. 441 | * 442 | * @return self 443 | */ 444 | private function cacheState() 445 | { 446 | if ($this->store) { 447 | $this->cache->forever("sanity.{$this->getKeyName()}", $this->store); 448 | } 449 | 450 | return $this; 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/Runners/RunnerForMiniGame.php: -------------------------------------------------------------------------------- 1 | filter(function ($runner, $key) use ($runnerNames) { 29 | return in_array($runner->getName(), $runnerNames, true); 30 | })->all(); 31 | } 32 | 33 | return $runners; 34 | } 35 | 36 | /** 37 | * Get the name of the pusher. 38 | * 39 | * @return string|null 40 | */ 41 | public function getPusherName() 42 | { 43 | return $this->getCommit()->author->name ?? null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Runners/ScoreboardRunner.php: -------------------------------------------------------------------------------- 1 | [ 35 | 'passing' => 2, 36 | 'fixed' => 5, 37 | 'failing' => -3, 38 | 'broken' => -10, 39 | ], 40 | 'Unit' => [ 41 | 'passing' => 2, 42 | 'fixed' => 5, 43 | 'failing' => -3, 44 | 'broken' => -10, 45 | ], 46 | 'Style' => [ 47 | 'passing' => 2, 48 | 'fixed' => 10, 49 | 'failing' => -20, 50 | 'broken' => -25, 51 | ], 52 | ]; 53 | 54 | /** 55 | * Runner execution. 56 | * 57 | * @return void 58 | */ 59 | protected function run() : void 60 | { 61 | $results = $this->getResults(); 62 | $runners = $this->getRealRunners(array_keys($this->pointsMap)); 63 | $pusher = $this->getPusherName(); 64 | $rState = ''; 65 | $pCount = 0; 66 | 67 | foreach ($runners as $runner) { 68 | if ($runner->wasJustFixed()) { 69 | $rState = 'fixed'; 70 | } elseif ($runner->wasJustBroken()) { 71 | $rState = 'broken'; 72 | } elseif ($runner->isCurrentlyPassing()) { 73 | $rState = 'passing'; 74 | } elseif ($runner->isCurrentlyFailing()) { 75 | $rState = 'failing'; 76 | } 77 | 78 | $pCount = $this->pointsMap[$runner->getName()][$rState]; 79 | $results = $this->updateScore($pusher, $results, $pCount); 80 | } 81 | 82 | $results = $this->updateRules($results); 83 | $results = $this->sortPlayersByPoints($results); 84 | $results = $this->createLabelForBadge($results); 85 | 86 | $this->setResults($results); 87 | } 88 | 89 | /** 90 | * Allocate points to player and return result. 91 | * 92 | * @param string $pusher The pusher name, 93 | * @param array $results The existing results set. 94 | * @param int $points The number of points to allocate. 95 | * 96 | * @return array 97 | */ 98 | private function updateScore($pusher, $results, $points) 99 | { 100 | if (!isset($results['players'][$pusher])) { 101 | $results['players'][$pusher] = $points; 102 | } else { 103 | $results['players'][$pusher] += $points; 104 | } 105 | 106 | return $results; 107 | } 108 | 109 | /** 110 | * Sort players by points in descening order. 111 | * 112 | * @param array $results The existing result set. 113 | * 114 | * @return array 115 | */ 116 | private function sortPlayersByPoints(array $results) 117 | { 118 | $results['players'] = collect($results['players'])->sortByDesc(function ($value, $key) { 119 | return $value; 120 | })->all(); 121 | 122 | return $results; 123 | } 124 | 125 | /** 126 | * Create displayable label for runner badge. 127 | * 128 | * @param array $results The existing result set. 129 | * 130 | * @return array 131 | */ 132 | private function createLabelForBadge(array $results) 133 | { 134 | $topPlayers = array_slice($results['players'], 0, 3); 135 | $topPlayersTemp = []; 136 | 137 | foreach ($topPlayers as $leader => $points) { 138 | $topPlayersTemp[] = $leader.' ('.number_format($points).')'; 139 | } 140 | 141 | $results['badge'] = str_replace('-', '--', implode(', ', $topPlayersTemp)); 142 | 143 | return $results; 144 | } 145 | 146 | /** 147 | * Add the point map allocation to the result. 148 | * 149 | * @param array $results The existing result set. 150 | * 151 | * @return array 152 | */ 153 | private function updateRules(array $results) 154 | { 155 | $results['rules'] = $this->pointsMap; 156 | 157 | return $results; 158 | } 159 | 160 | /** 161 | * Get mini game players. 162 | * 163 | * @return array 164 | */ 165 | public function getPlayers() 166 | { 167 | return $this->getResults()['players'] ?? []; 168 | } 169 | 170 | /** 171 | * Get mini game rules. 172 | * 173 | * @return array 174 | */ 175 | public function getRules() 176 | { 177 | return $this->getResults()['rules'] ?? $this->pointsMap; 178 | } 179 | 180 | /** 181 | * Get badge status. 182 | * 183 | * @return string 184 | */ 185 | public function getBadgeStatus() 186 | { 187 | $results = $this->getResults(); 188 | 189 | return rawurlencode($results['badge'] ?? 'none'); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Runners/StyleTestRunner.php: -------------------------------------------------------------------------------- 1 | setResults($results ?? []); 40 | 41 | if ( 42 | isset($results['totals']['errors']) && $results['totals']['errors'] == 0 && 43 | isset($results['totals']['warnings']) && $results['totals']['warnings'] == 0 44 | ) { 45 | $this->markAsPassed(); 46 | } else { 47 | $this->markAsFailed(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Runners/UnitTestRunner.php: -------------------------------------------------------------------------------- 1 | setResults($results); 41 | 42 | ($code == 0) ? $this->markAsPassed() : $this->markAsFailed(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/SanityServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 28 | __DIR__.'/Config/config.php' => config_path('sanity.php'), 29 | ], 'config'); 30 | 31 | $this->publishes([ 32 | __DIR__.'/Config/phpcs.xml' => base_path('phpcs.xml'), 33 | ], 'config'); 34 | 35 | if ($this->app->runningInConsole()) { 36 | $this->commands([ 37 | \Sanity\Commands\SanityMock::class, 38 | \Sanity\Commands\SanityReset::class, 39 | ]); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/SlackMessage.php: -------------------------------------------------------------------------------- 1 | [], 23 | ]; 24 | 25 | /** 26 | * New instance of SlackMessage. 27 | * 28 | * @param string $webhook Webhook URL to post to. 29 | * 30 | * @return void 31 | */ 32 | public function __construct($webhook) 33 | { 34 | $this->webhook = $webhook; 35 | } 36 | 37 | /** 38 | * Set a raw payload. 39 | * 40 | * @return self 41 | */ 42 | public function raw(array $raw) 43 | { 44 | $this->payload = $raw; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * Set username. 51 | * 52 | * @return self 53 | */ 54 | public function username($username) 55 | { 56 | $this->payload['username'] = $username; 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Set attachment title. 63 | * 64 | * @return self 65 | */ 66 | public function title($title) 67 | { 68 | $this->payload['attachments'][0]['title'] = $title; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * Set attachment text. 75 | * 76 | * @return self 77 | */ 78 | public function text($text) 79 | { 80 | $this->payload['attachments'][0]['text'] = $text; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * Set attachment pretext. 87 | * 88 | * @return self 89 | */ 90 | public function pretext($pretext) 91 | { 92 | $this->payload['attachments'][0]['pretext'] = $pretext; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * Set attachment footer. 99 | * 100 | * @return self 101 | */ 102 | public function footer($footer) 103 | { 104 | $this->payload['attachments'][0]['footer'] = $footer; 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Set attachment colour to green. 111 | * 112 | * @return self 113 | */ 114 | public function success() 115 | { 116 | $this->payload['attachments'][0]['color'] = '#99cc00'; 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * Set attachment colour to red. 123 | * 124 | * @return self 125 | */ 126 | public function danger() 127 | { 128 | $this->payload['attachments'][0]['color'] = '#c53232'; 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Add attachment field. 135 | * 136 | * @return self 137 | */ 138 | public function field($title, $value, $short = true) 139 | { 140 | $this->payload['attachments'][0]['fields'][] = [ 141 | 'title' => $title, 142 | 'value' => $value, 143 | 'short' => $short, 144 | ]; 145 | 146 | return $this; 147 | } 148 | 149 | /** 150 | * Add attachment action. 151 | * 152 | * @return self 153 | */ 154 | public function action($type, $text, $url, $style = '') 155 | { 156 | $this->payload['attachments'][0]['actions'][] = [ 157 | 'type' => $type, 158 | 'text' => $text, 159 | 'url' => $url, 160 | 'style'=> $style, 161 | ]; 162 | 163 | return $this; 164 | } 165 | 166 | /** 167 | * Send the message. 168 | * 169 | * @return void 170 | */ 171 | public function send() 172 | { 173 | $this->payload['attachments'][0]['ts'] = time(); 174 | 175 | Zttp::asJson()->post($this->webhook, $this->payload); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/Subscriber.php: -------------------------------------------------------------------------------- 1 | runner; 17 | $runnerState = $runnerClass->passing() ? 'Success' : 'Failure'; 18 | $runnerMethod = "on{$runnerClass->getName()}{$runnerState}"; 19 | 20 | if (method_exists($this, $runnerMethod)) { 21 | $this->$runnerMethod($runnerClass); 22 | } 23 | } 24 | 25 | /** 26 | * Get new instance of SlackMessage. 27 | * 28 | * @param string $webhook Webhook URL to post to. 29 | * 30 | * @return SlackMessage 31 | */ 32 | public function slack($webhook) 33 | { 34 | return new SlackMessage($webhook); 35 | } 36 | 37 | /** 38 | * Register the listeners for the subscriber. 39 | * 40 | * @param \Illuminate\Events\Dispatcher $events 41 | */ 42 | public function subscribe($events) 43 | { 44 | $subscriber = config('sanity.subscriber', get_class()); 45 | 46 | $events->listen('Sanity\Events\RunnerEvent', "{$subscriber}@onRunnerFinished"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudcake/laravel-sanity/5b60c67e206ded0f82343070918beb615d1d08e6/tests/.gitkeep -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'testbench'); 24 | $app['config']->set('database.connections.testbench', [ 25 | 'driver' => 'sqlite', 26 | 'database' => ':memory:', 27 | 'prefix' => '', 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Unit/SanityTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 12 | } 13 | } 14 | --------------------------------------------------------------------------------