├── .gitignore ├── LICENSE ├── README.md ├── asana_integration ├── README.md └── asana_integration.php ├── autopilot ├── README.md ├── autopilot.json └── autopilot.php ├── chikka-sms-notification ├── README.md ├── chikka_sms_notification.php └── example_pantheon.yml ├── cloudflare_cache ├── README.md ├── cloudflare_cache.json └── cloudflare_cache.php ├── db_sanitization ├── README.md ├── db_sanitization_drupal.php └── db_sanitization_wordpress.php ├── debugging_example ├── README.md └── debug.php ├── diffy_visualregression ├── README.md └── diffyVisualregression.php ├── drush_config_import ├── README.md └── drush_config_import.php ├── drush_revert_features ├── README.md └── revert_all_features.php ├── enable_dev_modules ├── README.md └── enable_dev_modules.php ├── example.pantheon.yml ├── generate_dev_content ├── README.md └── generate_dev_content.php ├── google_chat_notification ├── README.md └── google_chat_notification.php ├── jenkins ├── README.md ├── example.secrets.json └── jenkins_integration.php ├── jira_integration ├── README.md └── jira_integration.php ├── new_relic_apdex_t ├── README.md └── new_relic_apdex_t.php ├── new_relic_deploy ├── README.md └── new_relic_deploy.php ├── new_relic_monitor ├── README.md └── new_relic_monitor.php ├── pivotal-tracker ├── README.md └── pivotal_integration.php ├── quicksilver_pushback └── README.md ├── slack_notification ├── README.md └── slack_notification.php ├── teams_notification ├── README.md ├── samples │ ├── clear_cache_msg.json │ ├── deploy_msg.json │ └── sync_code_msg.json └── teams_notification.php ├── trello_integration ├── README.md └── trello_integration.php ├── url_checker ├── README.md ├── config.json └── url_checker.php ├── webhook ├── README.md └── webhook.php ├── wp_cfm_import ├── README.md ├── alter_wpcfm_config_path.php └── wp_cfm_after_clone.php ├── wp_search_replace ├── README.md └── wp_search_replace.php └── wp_solr_index ├── README.md └── wp_solr_power_index.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | vendor/ 3 | /.idea 4 | /.vscode 5 | 6 | # Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file 7 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 8 | # composer.lock 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Pantheon 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pantheon Cloud Integration Examples 2 | This repo contains example scripts for use with Quicksilver Platform Hooks. These will allow you to automate more of your workflow, and integrate better with other cloud services. 3 | 4 | [![Minimal Support](https://img.shields.io/badge/Pantheon-Minimal_Support-yellow?logo=pantheon&color=FFDC28)](https://pantheon.io/docs/oss-support-levels#minimal-support) 5 | 6 | The current release of Quicksilver supports one utility operation: `webphp`. This invokes a PHP script via the same runtime environment as the website itself. `php` scripts are subject to the same limits as any code on the platform, [like timeouts](https://pantheon.io/docs/articles/sites/timeouts/#timeouts-that-aren't-configurable), and cannot be batched. 7 | 8 | This initial release makes four platform workflows eligible for Quicksilver operations: 9 | 10 | - `deploy`: when code is deployed to Test or Live. `webphp` scripts run on the target environment. 11 | - `sync_code`: code is pushed via Git or committed in the Pantheon dashboard. `webphp` scripts run on the committed-to environment (dev or multidev). 12 | - `clone_database`: data is cloned between environments. `webphp` scripts run on the target (to_env) environment. 13 | - `clear_cache`: the most popular workflow of them all! `webphp` scripts run on the cleared environment. 14 | 15 | ## Introducing `pantheon.yml` ## 16 | 17 | Quicksilver is configured via a `pantheon.yml` file, which lives in the root of your repository (`~/code/`). When this file is first pushed to an environment, it will set up the workflow triggers. 18 | 19 | The format for `pantheon.yml` is as follows: 20 | 21 | ```yaml 22 | # Always start with an API version. This will increment as Quicksilver evolves. 23 | api_version: 1 24 | 25 | # Now specify the workflows to which you want to hook operations. 26 | workflows: 27 | deploy: 28 | # Each workflow can have a before and after operation. 29 | after: 30 | # For now, the only "type" available is webphp. 31 | - type: webphp 32 | # This will show up in output to help you keep track of your operations. 33 | description: Log to New Relic 34 | # This is (obviously) the path to the script. 35 | script: private/scripts/new_relic_deploy.php 36 | ``` 37 | 38 | Note that if you want to hook onto deploy workflows, you'll need to deploy your `pantheon.yml` into an environment first. Likewise, if you are adding new operations or changing the script an operation will target, the deploy which contains those adjustments to `pantheon.yml` will not self-referentially exhibit the new behavior. Only subsequent deploys will be affected. 39 | 40 | **When Updating:** 41 | **pantheon.yml**: Updates will fire on the next sequential workflow, not post-deploy. 42 | **scripts**: Updates will fire post-deploy. 43 | **script location**: Updates will fire on next sequential workflow, not post-deploy. 44 | 45 | **When Adding:** 46 | **pantheon.yml**: Updates will fire on the next sequential workflow, not post-deploy. 47 | **scripts**: Updates will fire on the next sequential workflow. 48 | 49 | ## Security ## 50 | 51 | When getting started with Quicksilver scripts, you'll want to first create **two** `private` directories on your website instance. 52 | 53 | The first `private` directory should be created in your `~/files/` directory via SFTP (e.g. `~/files/private/`). This directory is not included in your source code and is used to store a `secrets.json` file where you can confidently store sensitive information like API keys and credentials. You will need to create a separate `private` directory (and subsequent `secrets.json`) for each environment as this directory isn't included in source and will not propagate during deployments. You can easily manage the key-value pairs in the `secrets.json` file per environment (after initially creating the file via SFTP) using Terminus after installing the [Terminus Secrets Plugin](https://github.com/pantheon-systems/terminus-secrets-plugin). The Slack notification example uses this pattern. For high-security keys, we recommend a third party secrets lockbox like [Lockr](https://lockr.io). 54 | 55 | The second `private` directory should be created in your project's web root (e.g. `~/code/private/` OR `~/code/web/private/` depending on the `web_docroot` setting in your `pantheon.yml` file). This `private` directory is part of your repository, so it should not hold any sensitive information like API keys or credentials. Once you've created the `private` directory, we recommend creating a `scripts` directory within it to store all of your Quicksilver scripts. 56 | 57 | Pantheon automatically limits public access to both of these `private` directories, so no special configuration in `pantheon.yml` is required. Scripts stored here can only be executed by the Pantheon platform. 58 | 59 | ## Terminus Commands ## 60 | 61 | Developers making use of Quicksilver will want to make sure they are Terminus savvy. Get the latest release, and a few new commands are included: 62 | 63 | ```shell 64 | $ terminus help workflows 65 | ##NAME 66 | terminus workflows 67 | 68 | ##DESCRIPTION 69 | Actions to be taken on an individual site 70 | 71 | ##SYNOPSIS 72 | 73 | 74 | ##SUBCOMMANDS 75 | list 76 | List workflows for a site 77 | show 78 | Show operation details for a workflow 79 | watch 80 | Streams new and finished workflows to the console 81 | ``` 82 | 83 | The `list` and `show` commands will allow you to explore previous workflows and their Quicksilver operations. The `watch` command is a developers best friend: it will set up Terminus to automatically "follow" the workflow activity of your site, dumping back any Quicksilver output along with them. 84 | 85 | ## Environment variables ## 86 | 87 | To discover what environment variables are available to your scripts then take a look at the [debugging_example](debugging_example) script and instructions. 88 | 89 | ## Troubleshooting ## 90 | 91 | - While your scripts can live anywhere, we recommend `private` since that will prevent the contents from ever being directly accessed via the public internet. 92 | - You'll know `pantheon.yml` has been added correctly, and your quicksilver actions are registered when you see a message like the following on `git push`: 93 | ``` 94 | remote: PANTHEON NOTICE: 95 | remote: 96 | remote: Changes to `pantheon.yml` detected. 97 | remote: 98 | remote: Successfully applied `pantheon.yml` to the 'dev' environment. 99 | ``` 100 | -------------------------------------------------------------------------------- /asana_integration/README.md: -------------------------------------------------------------------------------- 1 | # Asana Integration # 2 | 3 | This example parses commit messages for Asana task IDs and adds the commit message as a comment in the related Asana task. 4 | 5 | Example comments: 6 | 7 | [389749465118801]: Adjust layout spacing 8 | 9 | Commits that contain multiple Asana tasks will post comments to each issue mentioned. A comment will be added each time a commit is pushed to any dev or multidev branch; each Asana comment is labeled with the appropriate commit hash and Pantheon environment that triggered the post. 10 | 11 | ## Instructions ## 12 | 13 | - In Asana, go to My Profile Settings -> Apps -> Manage Developer Apps -> Create New Personal Access Token and copy the new token. 14 | - Store the Asana Personal Access Token into a file called `secrets.json` and store it in the private files area of your site 15 | 16 | ```shell 17 | $> echo '{"asana_access_token" : "Your generated Personal Access Token" }' > secrets.json 18 | # Note, you'll need to copy the secrets into each environment where you want to save commit messages to Asana 19 | $> `terminus site connection-info --env=dev --site=your-site --field=sftp_command` 20 | Connected to appserver.dev.d1ef01f8-364c-4b91-a8e4-f2a46f14237e.drush.in. 21 | sftp> cd files 22 | sftp> mkdir private 23 | sftp> cd private 24 | sftp> put secrets.json 25 | 26 | ``` 27 | - Add the example `asana_integration.php` script to the `private` directory of your code repository. 28 | - Add a Quicksilver operation to your `pantheon.yml` to fire the script after a deploy. 29 | - Push code with a commit message containing an Asana task ID! 30 | 31 | Note: If you open the task in Asana the URL will look something like this: https://app.asana.com/0/389749465118800/389749465118801 32 | The second number is your task ID. Surround it with [] in your commits. 33 | 34 | Optionally, you may want to use the `terminus workflow:watch` command to get immediate debugging feedback. 35 | 36 | ### Example `pantheon.yml` ### 37 | 38 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 39 | 40 | ```yaml 41 | api_version: 1 42 | 43 | workflows: 44 | sync_code: 45 | after: 46 | - type: webphp 47 | description: Asana Integration 48 | script: private/scripts/asana_integration.php 49 | ``` 50 | -------------------------------------------------------------------------------- /asana_integration/asana_integration.php: -------------------------------------------------------------------------------- 1 | /dev/null", $output, $status); 26 | if (!$status) { 27 | $last_commithash = $last_processed_commithash; 28 | } 29 | } 30 | // Update the last commit file with the latest commit 31 | file_put_contents($commit_file, $current_commithash, LOCK_EX); 32 | 33 | // Retrieve git log for commits after last processed, to current 34 | $commits = _get_commits($current_commithash, $last_commithash, $env); 35 | 36 | // Check each commit message for Asana task IDs 37 | foreach ($commits['asana'] as $task_id => $commit_ids) { 38 | foreach ($commit_ids as $commit_id) { 39 | send_commit($secrets, $task_id, $commits['history'][$commit_id]); 40 | } 41 | } 42 | 43 | /** 44 | * Do git operations to find all commits between the specified commit hashes, 45 | * and return an associative array containing all applicable commits that 46 | * contain references to Asana tasks. 47 | */ 48 | function _get_commits($current_commithash, $last_commithash, $env) { 49 | $commits = array( 50 | // Raw output of git log since the last processed 51 | 'history_raw' => null, 52 | // Formatted array of commits being sent to Asana 53 | 'history' => array(), 54 | // An array keyed by Asana task id, each holding an 55 | // array of commit ids. 56 | 'asana' => array() 57 | ); 58 | 59 | $cmd = 'git log'; // add -p to include diff 60 | if (!$last_commithash) { 61 | $cmd .= ' -n 1'; 62 | } 63 | else { 64 | $cmd .= ' ' . $last_commithash . '...' . $current_commithash; 65 | } 66 | $commits['history_raw'] = shell_exec($cmd); 67 | // Parse raw history into an array of commits 68 | $history = preg_split('/^commit /m', $commits['history_raw'], -1, PREG_SPLIT_NO_EMPTY); 69 | foreach ($history as $str) { 70 | $commit = array( 71 | 'full' => 'Commit: ' . $str 72 | ); 73 | // Only interested in the lines before the diff now 74 | $lines = explode("\n", $str); 75 | $commit['id'] = $lines[0]; 76 | $commit['message'] = trim(implode("\n", array_slice($lines, 4))); 77 | $commit['formatted'] = 'Commit: ' . substr($commit['id'], 0, 10) . ' [' . $env . '] 78 | ' . $commit['message'] . ' 79 | ~' . $lines[1] . ' - ' . $lines[2]; 80 | // Look for matches on a Asana task ID format 81 | // = [number] 82 | preg_match('/\[[0-9]+\]/', $commit['message'], $matches); 83 | if (count($matches) > 0) { 84 | // Build the $commits['asana'] array so there is 85 | // only 1 item per ticket id 86 | foreach ($matches as $task_id_enc) { 87 | $task_id = substr($task_id_enc, 1, -1); 88 | if (!isset($commits['asana'][$task_id])) { 89 | $commits['asana'][$task_id] = array(); 90 | } 91 | // ... and only 1 item per commit id 92 | $commits['asana'][$task_id][$commit['id']] = $commit['id']; 93 | } 94 | // Add the commit to the history array since there was a match. 95 | $commits['history'][$commit['id']] = $commit; 96 | } 97 | } 98 | return $commits; 99 | } 100 | 101 | /** 102 | * Send commits to Asana 103 | */ 104 | function send_commit($secrets, $task_id, $commit) { 105 | $payload = array( 106 | 'text' => $commit['formatted'] 107 | ); 108 | $ch = curl_init(); 109 | curl_setopt($ch, CURLOPT_URL, 'https://app.asana.com/api/1.0/tasks/' . $task_id . '/stories'); 110 | curl_setopt($ch, CURLOPT_POST, 1); 111 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 112 | curl_setopt($ch, CURLOPT_TIMEOUT, 5); 113 | curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); 114 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Bearer '.$secrets['asana_access_token'])); 115 | print("\n==== Posting to Asana ====\n"); 116 | $result = curl_exec($ch); 117 | print("RESULT: $result"); 118 | print("\n===== Post Complete! =====\n"); 119 | curl_close($ch); 120 | } 121 | 122 | /** 123 | * Get secrets from secrets file. 124 | * 125 | * @param array $requiredKeys List of keys in secrets file that must exist. 126 | */ 127 | function _get_secrets($requiredKeys, $defaults) 128 | { 129 | $secretsFile = $_SERVER['HOME'] . '/files/private/secrets.json'; 130 | if (!file_exists($secretsFile)) { 131 | die('No secrets file ['.$secretsFile.'] found. Aborting!'); 132 | } 133 | $secretsContents = file_get_contents($secretsFile); 134 | $secrets = json_decode($secretsContents, 1); 135 | if ($secrets == false) { 136 | die('Could not parse json in secrets file. Aborting!'); 137 | } 138 | $secrets += $defaults; 139 | $missing = array_diff($requiredKeys, array_keys($secrets)); 140 | if (!empty($missing)) { 141 | die('Missing required keys in json secrets file: ' . implode(',', $missing) . '. Aborting!'); 142 | } 143 | return $secrets; 144 | } 145 | -------------------------------------------------------------------------------- /autopilot/README.md: -------------------------------------------------------------------------------- 1 | # Autopilot # 2 | 3 | This example demonstrates how to leverage Autopilot variables in Quicksilver for notifications to Slack. A detailed message is sent to Slack after the VRT test runs, indicating which modules were updated, pass/fail the VRT test, and links to the site dashboard. 4 | 5 | ## Instructions ## 6 | 7 | - Copy `autopilot.json` to `files/private` of the *live* environment after updating it with your Slack info. 8 | - Modify the `webhook_url` in `autopilot.json` to be the desired organization's Slack webhook. 9 | - Add the example `autopilot.php` script to the `private/scripts` directory of your code repository. 10 | - Add a Quicksilver operation to your `pantheon.yml` to fire the script after an `autopilot_vrt`. 11 | - Deploy through to the live environment and queue updates! Optionally roll a module(s) back a version or 2 to force an update if none are available currently. 12 | 13 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. You may need to delete the `autopilot` multidev environment after making changes to your `pantheon.yml`. 14 | 15 | ### Example `pantheon.yml` ### 16 | 17 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 18 | 19 | ```yaml 20 | api_version: 1 21 | workflows: 22 | autopilot_vrt: 23 | after: 24 | - type: webphp 25 | description: Autopilot after 26 | script: private/scripts/autopilot.php 27 | ``` 28 | 29 | After running an Autopilot VRT, if any updates were applied, you should receive a VRT notification. -------------------------------------------------------------------------------- /autopilot/autopilot.json: -------------------------------------------------------------------------------- 1 | { 2 | "webhook_url": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 3 | } -------------------------------------------------------------------------------- /autopilot/autopilot.php: -------------------------------------------------------------------------------- 1 | $val) { 28 | $update_list_markdown .= "- " . $val['title'] . ' updated from ' . $val['version'] . ' to ' . $val['update_version'] . " \n"; 29 | } 30 | 31 | // Get other info for the message. 32 | $wf_type = $_POST['wf_type']; 33 | $user_email = $_POST['user_email']; 34 | $site_id = $_POST['site_id']; 35 | $vrt_result_url = $_POST['vrt_result_url']; 36 | $status = $_POST['vrt_status']; 37 | $full_vrt_url_path = "https://dashboard.pantheon.io/" . $vrt_result_url; 38 | $site_name = $_ENV['PANTHEON_SITE_NAME']; 39 | $site_url_and_label = 'http://' . $_ENV['PANTHEON_ENVIRONMENT'] . '-' . $_ENV['PANTHEON_SITE_NAME'] . '.pantheonsite.io|' . $_ENV['PANTHEON_ENVIRONMENT']; 40 | $qs_description = $_POST['qs_description']; 41 | 42 | // Set an emoji based on pass state - star for pass, red flag for fail 43 | $emoji = $status == "pass" ? ":star:" : ":red-flag:"; 44 | 45 | // Construct the data for Slack message API. 46 | $message_data = [ 47 | "text" => "Autopilot VRT for " . $site_name, 48 | "blocks" => [ 49 | [ 50 | "type" => "section", 51 | "text" => [ 52 | "type" => "mrkdwn", 53 | "text" => "$emoji VRT Results $status for $site_name - $full_vrt_url_path", 54 | ] 55 | ], 56 | [ 57 | "type" => "section", 58 | "block_id" => "section2", 59 | "text" => [ 60 | "type" => "mrkdwn", 61 | "text" => "<$full_vrt_url_path|Review VRT Results for $site_name> \n The test state was: $emoji $status $emoji." 62 | ], 63 | "accessory" => [ 64 | "type" => "image", 65 | "image_url" => "https://pantheon.io/sites/all/themes/zeus/images/new-design/homepage/home-hero-webops-large.jpg", 66 | "alt_text" => "Pantheon image" 67 | ] 68 | ], 69 | [ 70 | "type" => "section", 71 | "block_id" => "section3", 72 | "fields" => [ 73 | [ 74 | "type" => "mrkdwn", 75 | "text" => $update_list_markdown, 76 | ] 77 | ] 78 | ] 79 | ] 80 | ]; 81 | 82 | // Encode the data into JSON to send to Slack. 83 | $message_data = json_encode($message_data); 84 | 85 | // Define the command that will send the curl to the Slack webhook with the constructed data. 86 | $command = "curl -s -X POST -H 'Content-type: application/json' --data '" . $message_data . "' $webook_url"; 87 | 88 | // Execute the command with var_dump, so we can see output if running terminus workflow:watch, which can help for debugging. 89 | var_dump(shell_exec("$command 2>&1")); 90 | -------------------------------------------------------------------------------- /chikka-sms-notification/README.md: -------------------------------------------------------------------------------- 1 | #**SMS Notification using Chikka API** 2 | 3 | This script shows how easy it is to integrate Chikka SMS notifications from your Pantheon project using Quicksilver. As a bonus, we also show you how to manage API keys outside of your site repository. 4 | 5 | Instructions 6 | 7 | 1. [Sign up and Register your Chikka Application](https://api.chikka.com/docs/getting-started#register-your-application) from your Chikka SMS Website. 8 | 9 | 2. Copy the following variables: 10 | 11 | * chikka_client_id 12 | * chikka_client_secret 13 | * chikka_accesscode 14 | 15 | into a file called `secrets.json` and store it in the [private files](https://pantheon.io/docs/articles/sites/private-files/) directory of every environment where you want to trigger Chikka SMS notifications. 16 | 17 | ``` 18 | $> echo '{"chikka_client_id": "xxxxxxxxxxxxxxxxx", "chikka_client_secret": "xxxxxxxxxxxxxx", "chikka_accesscode": "xxxxxxxxxxxxxx"}' > secrets.json 19 | # Note, you'll need to copy the secrets into each environment where you want to trigger Chikka SMS notifications. 20 | $> `terminus site connection-info --env=dev --site=your-site --field=sftp_command` 21 | Connected to appserver.dev.xxxxxxx-xxxxxx-xxxxx-xxxxx.drush.in. 22 | sftp> cd files 23 | sftp> mkdir private 24 | sftp> cd private 25 | sftp> put secrets.json 26 | ``` 27 | 28 | 3. Add the example `chikka-sms-notification.php` script to the private directory in the root of your site's codebase, that is under version control. Note this is a different private directory than where the secrets.json is stored. 29 | 30 | 4. Add Quicksilver operations to your `pantheon.yml` 31 | 32 | 5. Test a deploy out! 33 | 34 | Optionally, you may want to use the terminus workflows watch command to get immediate debugging feedback. You may also want to customize your notifications further. 35 | 36 | Example pantheon.yml 37 | 38 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use. Pick and choose the exact workflows that you would like to see notifications for. 39 | 40 | ``` 41 | api_version: 1 42 | 43 | workflows: 44 | deploy: 45 | after: 46 | - type: webphp 47 | description: send sms on deploy 48 | script: private/scripts/chikka_sms_notification.php 49 | sync_code: 50 | after: 51 | - type: webphp 52 | description: send sms on sync code 53 | script: private/scripts/chikka_sms_notification.php 54 | clear_cache: 55 | after: 56 | - type: webphp 57 | description: send sms when clearing cache 58 | script: private/scripts/chikka_sms_notification.php 59 | ``` 60 | -------------------------------------------------------------------------------- /chikka-sms-notification/chikka_sms_notification.php: -------------------------------------------------------------------------------- 1 | 'https://post.chikka.com/smsapi/request', 5 | 'mobile_number' => 'xxxxxxxxxxxx', 6 | ); 7 | 8 | // Load our hidden credentials. 9 | // See the README.md for instructions on storing secrets. 10 | $secrets = _get_secrets(array('chikka_client_id', 'chikka_client_secret', 'chikka_accesscode'), $defaults); 11 | $number = $secrets['mobile_number']; 12 | 13 | $workflow_description = ucfirst($_POST['stage']) . ' ' . str_replace('_', ' ', $_POST['wf_type']); 14 | 15 | // Customize the message based on the workflow type. Note that chikka_sms_notification.php 16 | // must appear in your pantheon.yml for each workflow type you wish to send notifications on. 17 | switch($_POST['wf_type']) { 18 | case 'deploy': 19 | // Find out what tag we are on and get the annotation. 20 | $deploy_tag = `git describe --tags`; 21 | $deploy_message = $_POST['deploy_message']; 22 | // Prepare the message 23 | $text = $_POST['user_fullname'] . ' deployed ' . $_ENV['PANTHEON_SITE_NAME'] . ' 24 | On branch "' . PANTHEON_ENVIRONMENT . '"Workflow: ' . $workflow_description . ' 25 | Deploy Message: ' . htmlentities($deploy_message); 26 | break; 27 | case 'sync_code': 28 | // Get the committer, hash, and message for the most recent commit. 29 | $committer = `git log -1 --pretty=%cn`; 30 | $email = `git log -1 --pretty=%ce`; 31 | $message = `git log -1 --pretty=%B`; 32 | $hash = `git log -1 --pretty=%h`; 33 | // Prepare the message 34 | $text = $_POST['user_fullname'] . ' committed to' . $_ENV['PANTHEON_SITE_NAME'] . ' 35 | On branch "' . PANTHEON_ENVIRONMENT . '"Workflow: ' . $workflow_description . ' 36 | - ' . htmlentities($message); 37 | break; 38 | default: 39 | $text = "Workflow $workflow_description" . $_POST['qs_description']; 40 | break; 41 | } 42 | 43 | 44 | $message = $text; 45 | if ( sendSMS($number, $message, $secrets['chikka_accesscode'], $secrets['chikka_client_id'], $secrets['chikka_client_secret'], $secrets['chikka_url'] ) == true) { 46 | echo "Successfully sent SMS to $number"; 47 | } else { 48 | echo "ERROR"; 49 | } 50 | 51 | 52 | /** 53 | * Get secrets from secrets file. 54 | * 55 | * @param array $requiredKeys List of keys in secrets file that must exist. 56 | */ 57 | function _get_secrets($requiredKeys, $defaults) 58 | { 59 | $secretsFile = $_SERVER['HOME'] . '/files/private/secrets.json'; 60 | if (!file_exists($secretsFile)) { 61 | die('No secrets file found. Aborting!'); 62 | } 63 | $secretsContents = file_get_contents($secretsFile); 64 | $secrets = json_decode($secretsContents, 1); 65 | if ($secrets == false) { 66 | die('Could not parse json in secrets file. Aborting!'); 67 | } 68 | $secrets += $defaults; 69 | $missing = array_diff($requiredKeys, array_keys($secrets)); 70 | if (!empty($missing)) { 71 | die('Missing required keys in json secrets file: ' . implode(',', $missing) . '. Aborting!'); 72 | } 73 | return $secrets; 74 | } 75 | 76 | // Send / Broadcast SMS 77 | function sendSMS($mobile_number, $message, $chikka_accesscode, $chikka_client_id, $chikka_client_secret, $chikka_url) 78 | { 79 | $post = array( "message_type" => "SEND", 80 | "mobile_number" => $mobile_number, 81 | "shortcode" => $chikka_accesscode, 82 | "message_id" => date('YmdHis'), 83 | "message" => urlencode($message), 84 | "client_id" => $chikka_client_id, 85 | "secret_key" => $chikka_client_secret); 86 | 87 | $result = curl_request($chikka_url, $post); 88 | $result = json_decode($result, true); 89 | if ($result['status'] == '200') { 90 | return true; 91 | } else { 92 | return false; 93 | } 94 | } 95 | 96 | // Reply SMS 97 | function replySMS($mobile_number, $request_id, $message, $price = 'P2.50', $chikka_accesscode, $chikka_client_id, $chikka_client_secret, $chikka_url) 98 | { 99 | $message_id = date('YmdHis'); 100 | $post = array( "message_type" => "REPLY", 101 | "mobile_number" => $mobile_number, 102 | "shortcode" => $chikka_accesscode, 103 | "message_id" => $message_id, 104 | "message" => urlencode($message), 105 | "request_id" => $request_id, 106 | "request_cost" => $price, 107 | "client_id" => $chikka_client_id, 108 | "secret_key" => $chikka_client_secret); 109 | 110 | $result = curl_request($chikka_url, $post); 111 | $result = json_decode($result, true); 112 | if ($result['status'] == '200') { 113 | return true; 114 | } else { 115 | return false; 116 | } 117 | } 118 | 119 | // Reply SMS 120 | function replySMS2($mobile_number, $request_id, $message, $price = 'P2.50', $chikka_accesscode, $chikka_client_id, $chikka_client_secret, $chikka_url) 121 | { 122 | $message_id = date('YmdHis'); 123 | $post = array( "message_type" => "REPLY", 124 | "mobile_number" => $mobile_number, 125 | "shortcode" => $secrets['chikka_accesscode'], 126 | "message_id" => $message_id, 127 | "message" => urlencode($message), 128 | "request_id" => $request_id, 129 | "request_cost" => $price, 130 | "client_id" => $secrets['chikka_client_id'], 131 | "secret_key" => $secrets['chikka_client_secret'] ); 132 | 133 | $result = curl_request($secrets['chikka_url'], $post); 134 | $result = json_decode($result, true); 135 | if ($result['status'] == '200') { 136 | return true; 137 | } else { 138 | return false; 139 | } 140 | } 141 | 142 | // Basic Curl Request 143 | function curl_request( $URL, $arr_post_body) 144 | { 145 | $query_string = ""; 146 | foreach($arr_post_body as $key => $frow) { 147 | $query_string .= '&'.$key.'='.$frow; 148 | } 149 | 150 | $curl_handler = curl_init(); 151 | curl_setopt($curl_handler, CURLOPT_URL, $URL); 152 | curl_setopt($curl_handler, CURLOPT_POST, count($arr_post_body)); 153 | curl_setopt($curl_handler, CURLOPT_POSTFIELDS, $query_string); 154 | curl_setopt($curl_handler, CURLOPT_RETURNTRANSFER, true); 155 | $response = curl_exec($curl_handler); 156 | 157 | if(curl_errno($curl_handler)) 158 | { 159 | $info = curl_getinfo($curl_handler); 160 | } 161 | curl_close($curl_handler); 162 | 163 | return $response; 164 | } 165 | 166 | ?> 167 | -------------------------------------------------------------------------------- /chikka-sms-notification/example_pantheon.yml: -------------------------------------------------------------------------------- 1 | api_version: 1 2 | 3 | workflows: 4 | deploy: 5 | after: 6 | - type: webphp 7 | description: send sms on deploy 8 | script: private/scripts/chikka_sms_notification.php 9 | sync_code: 10 | after: 11 | - type: webphp 12 | description: send sms on sync code 13 | script: private/scripts/chikka_sms_notification.php 14 | clear_cache: 15 | after: 16 | - type: webphp 17 | description: send sms when clearing cache 18 | script: private/scripts/chikka_sms_notification.php 19 | -------------------------------------------------------------------------------- /cloudflare_cache/README.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Cache # 2 | 3 | This example demonstrates how to purge Cloudflare cache when your live environment's cache is cleared. 4 | 5 | ## Instructions ## 6 | 7 | - Copy `cloudflare_cache.json` to `files/private` of the *live* environment after updating it with your cloudflare info. 8 | - API key can be found in the `My Settings` page on the Cloudflare site. 9 | - I couldn't find zone id in the UI. I viewed page source on the overview page and found it printed in JavaScript. 10 | - Add the example `cloudflare_cache.php` script to the `private/scripts` directory of your code repository. 11 | - Add a Quicksilver operation to your `pantheon.yml` to fire the script after a deploy. 12 | - Deploy through to the live environment and clear the cache! 13 | 14 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. 15 | 16 | ### Example `pantheon.yml` ### 17 | 18 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 19 | 20 | ```yaml 21 | api_version: 1 22 | 23 | workflows: 24 | clear_cache: 25 | after: 26 | - type: webphp 27 | description: Cloudflare Cache 28 | script: private/scripts/cloudflare_cache.php 29 | ``` 30 | 31 | Note that you will almost always want to clear your CDN cache with the _after_ timing option. Otherwise you could end up with requests re-caching stale content. Caches should generally be cleared "bottom up". -------------------------------------------------------------------------------- /cloudflare_cache/cloudflare_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "zone_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 3 | "api_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 4 | "email": "cloudflare.user@example.com" 5 | } -------------------------------------------------------------------------------- /cloudflare_cache/cloudflare_cache.php: -------------------------------------------------------------------------------- 1 | true)); 21 | $ch = curl_init(); 22 | curl_setopt($ch, CURLOPT_URL, 'https://api.cloudflare.com/client/v4/zones/' . $config['zone_id'] . '/purge_cache'); 23 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); 24 | curl_setopt($ch, CURLOPT_HTTPHEADER, array( 25 | 'X-Auth-Email: ' . $config['email'], 26 | 'X-Auth-Key: ' . $config['api_key'], 27 | 'Content-Type: application/json' 28 | )); 29 | curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); 30 | print("\n==== Sending Request to Cloudflare ====\n"); 31 | $result = curl_exec($ch); 32 | print("RESULT: $result"); 33 | print("\n===== Request Complete! =====\n"); 34 | curl_close($ch); 35 | } 36 | 37 | -------------------------------------------------------------------------------- /db_sanitization/README.md: -------------------------------------------------------------------------------- 1 | # Database sanitization of user emails and passwords. # 2 | 3 | This example will show you how you can automatically sanitize emails and passwords from your Drupal or WordPress database when cloning to a different environment. This practice can help prevent against the accidental exposure of sensitive data by making it easy for your team members to use and download sanitized databases. The Pantheon backups of the live environment will still contain user emails and hashed passwords. 4 | 5 | ## Instructions ## 6 | 7 | Setting up this example is easy: 8 | 9 | 1. Add either the db_sanitization_drupal.php or the db_sanitization_wordpress.php to the `private` directory of your code repository. 10 | 2. Add a Quicksilver operation to your `pantheon.yml` to fire the script after cloning the database. 11 | 3. Test a deploy out! 12 | 13 | Optionally, you may want to use the `terminus workflow:watch yoursitename` command to get immediate debugging feedback. 14 | 15 | ### Example `pantheon.yml` ### 16 | 17 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 18 | 19 | ```yaml 20 | 21 | api_version: 1 22 | 23 | workflows: 24 | clone_database: 25 | after: 26 | - type: webphp 27 | description: Sanitize the db 28 | script: private/scripts/db_sanitization_(wordpress|drupal).php 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /db_sanitization/db_sanitization_drupal.php: -------------------------------------------------------------------------------- 1 | query("UPDATE wp_users SET user_email = CONCAT(user_login, '@localhost'), user_pass = MD5(CONCAT('MILDSECRET', user_login)), user_activation_key = '';"); 11 | } 12 | -------------------------------------------------------------------------------- /debugging_example/README.md: -------------------------------------------------------------------------------- 1 | # Quicksilver Debugging # 2 | 3 | This example is intended for users who want to explore the potential for Quicksilver with a quick debugging example. 4 | 5 | Setting up this example is easy: 6 | 7 | 1. Add the example `debug.php` script to the `private` directory of your code repository. 8 | 2. Add a Quicksilver operation to your `pantheon.yml` to fire the script after cache clears. 9 | 3. Fire up terminus to watch the workflow log. 10 | 4. Push everything to Pantheon. 11 | 5. Clear the caches and see the output! 12 | 13 | ### Example `pantheon.yml` ### 14 | 15 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 16 | 17 | ```yaml 18 | api_version: 1 19 | 20 | workflows: 21 | clear_cache: 22 | after: 23 | - type: webphp 24 | description: Dump debugging output 25 | script: private/scripts/debug.php 26 | ``` 27 | 28 | ### Example `terminus workflow:watch` Output ### 29 | 30 | Triggering cache clears from your dashboard you should enjoy nice debugging output like this: 31 | 32 | ```shell 33 | $> terminus workflow:watch your-site-name 34 | [2015-12-15 03:17:26] [info] Watching workflows... 35 | [2015-12-15 03:17:50] [info] Started 1c5421b8-a2db-11e5-8a28-bc764e10b0ce Clear cache for "dev" (dev) 36 | [2015-12-15 03:17:58] [info] Finished Workflow 1c5421b8-a2db-11e5-8a28-bc764e10b0ce Clear cache for "dev" (dev) 37 | [2015-12-15 03:18:00] [info] 38 | ------ Operation: Dump debugging output finished in 2s ------ 39 | Quicksilver Debuging Output 40 | 41 | 42 | ========= START PAYLOAD =========== 43 | Array 44 | ( 45 | [wf_type] => clear_cache 46 | [user_id] => ed828d9d-2389-4e8d-9f71-bd2fcafc93c2 47 | [site_id] => 6c5ee454-9427-4cce-8193-a44d6c54172c 48 | [user_role] => owner 49 | [trace_id] => 1c4b90c0-a2db-11e5-9ca4-efb1318547fc 50 | [environment] => dev 51 | [wf_description] => Clear cache for "dev" 52 | [user_email] => josh@getpantheon.com 53 | ) 54 | 55 | ========== END PAYLOAD ============ 56 | ``` 57 | 58 | The `wf_type`, `wf_description` and `user_email` values are likely to be of particular interest. You can get additional information from the `$_SERVER` and `$_ENV` superglobals. You can also interrogate the status of the git repository, as well as bootstrapping the CMS. There are lots of possibilities! Have fun with Quicksilver! 59 | -------------------------------------------------------------------------------- /debugging_example/debug.php: -------------------------------------------------------------------------------- 1 | $value) { 12 | if (preg_match('#(PASSWORD|SALT|AUTH|SECURE|NONCE|LOGGED_IN)#', $key)) { 13 | $env[$key] = '[REDACTED]'; 14 | } 15 | } 16 | print_r($env); 17 | echo "\n-------- END ENVIRONMENT ----------\n"; 18 | -------------------------------------------------------------------------------- /diffy_visualregression/README.md: -------------------------------------------------------------------------------- 1 | # Visual Regression Testing via Diffy.website # 2 | 3 | This example will show you how to integrate [Diffy.website](http://Diffy.website)'s visual regression operation into your deployment workflow. 4 | 5 | This will allow you to do a comparative visual diff between the live environment and the test environemnt everytime you deploy to the testing environment. 6 | 7 | For more advanced use cases, including doing visual regression against Multidev instances, this script can be easily adapted for Diffy.website's REST API: [https://diffy.website/rest](https://diffy.website/rest). 8 | 9 | ## Instructions ## 10 | 11 | Vide demo is available https://youtu.be/U8uHJELeTDE. 12 | 13 | In order to get up and running, you first need to setup a Diffy.website project: 14 | 15 | 1. Either login to your account or register for a new one at [https://diffy.website](https://diffy.website). 16 | 2. Setup a Diffy project for your site and define the Production and Staging URLs in the project settings. 17 | 18 | Then you need to add the relevant code to your Pantheon project: 19 | 20 | 1. Add the example `diffyVisualregression.php` script to the 'private/scripts/' directory of your code repository. 21 | 2. Create an API token in Diffy (). Copy the token and project_id into a file called `secrets.json` and store it in the [private files](https://pantheon.io/docs/articles/sites/private-files/) directory. 22 | 23 | ```shell 24 | $> echo '{"token": "yourToken", "project_id" : "123"}' > secrets.json 25 | sftp YOURCREDENTIALS_TO_LIVE_ENVIRONMENT 26 | sftp> cd files 27 | sftp> mkdir private 28 | sftp> cd private 29 | sftp> put secrets.json 30 | sftp> quit 31 | ``` 32 | 33 | 3. Add a Quicksilver operation to your `pantheon.yml` to fire the script after a deploy to test. 34 | ``` 35 | api_version: 1 36 | 37 | workflows: 38 | deploy: 39 | after: 40 | - type: webphp 41 | description: Do a visual regression test with Diffy.website 42 | script: private/scripts/diffyVisualregression.php 43 | ``` 44 | 4. Make a deploy to test environment! 45 | 46 | Optionally, you may want to use the `terminus workflows watch YOUR_SITE_ID` command to get immediate debugging feedback. First you would need to install and authenticate your terminus. 47 | -------------------------------------------------------------------------------- /diffy_visualregression/diffyVisualregression.php: -------------------------------------------------------------------------------- 1 | run(); 12 | } 13 | else { 14 | echo 'No it is not Test environment. Skipping visual testing.' . PHP_EOL; 15 | } 16 | 17 | class DiffyVisualregression { 18 | 19 | private $jwt; 20 | private $error; 21 | private $processMsg = ''; 22 | private $secrets; 23 | 24 | public function run() 25 | { 26 | 27 | // Load our hidden credentials. 28 | // See the README.md for instructions on storing secrets. 29 | $this->secrets = $this->get_secrets(['token', 'project_id']); 30 | 31 | echo 'Starting a visual regression test between the live and test environments...' . PHP_EOL; 32 | $isLoggedIn = $this->login($this->secrets['token']); 33 | if (!$isLoggedIn) { 34 | echo $this->error; 35 | return; 36 | } 37 | 38 | $compare = $this->compare(); 39 | if (!$compare) { 40 | echo $this->error; 41 | return; 42 | } 43 | 44 | echo $this->processMsg; 45 | return; 46 | } 47 | 48 | private function compare() 49 | { 50 | $curl = curl_init(); 51 | $authorization = 'Authorization: Bearer ' . $this->jwt; 52 | $curlOptions = array( 53 | CURLOPT_URL => rtrim(SITE_URL, '/') . '/api/projects/' . $this->secrets['project_id'] . '/compare', 54 | CURLOPT_HTTPHEADER => array('Content-Type: application/json' , $authorization ), 55 | CURLOPT_POST => 1, 56 | CURLOPT_RETURNTRANSFER => 1, 57 | CURLOPT_POSTFIELDS => json_encode(array( 58 | 'env1' => 'prod', 59 | 'env2' => 'stage', 60 | 'withRescan' => false 61 | )) 62 | ); 63 | 64 | curl_setopt_array($curl, $curlOptions); 65 | $curlResponse = json_decode(curl_exec($curl)); 66 | $curlErrorMsg = curl_error($curl); 67 | $curlErrno= curl_errno($curl); 68 | curl_close($curl); 69 | 70 | if ($curlErrorMsg) { 71 | $this->error = $curlErrno . ': ' . $curlErrorMsg . '\n'; 72 | return false; 73 | } 74 | 75 | if (isset($curlResponse->errors)) { 76 | 77 | $errorMessages = is_object($curlResponse->errors) ? $this->parseProjectErrors($curlResponse->errors) : $curlResponse->errors; 78 | $this->error = '-1:' . $errorMessages; 79 | return false; 80 | } 81 | 82 | if (strstr($curlResponse, 'diff: ')) { 83 | $diffId = (int) str_replace('diff: ', '', $curlResponse); 84 | if ($diffId) { 85 | $this->processMsg .= 'Check out the result here: ' . rtrim(SITE_URL, '/') . '/#/diffs/' . $diffId . PHP_EOL; 86 | return true; 87 | } 88 | } else { 89 | $this->error = '-1:' . $curlResponse . PHP_EOL; 90 | return false; 91 | } 92 | } 93 | 94 | private function login($token) { 95 | $curl = curl_init(); 96 | $curlOptions = array( 97 | CURLOPT_URL => rtrim(SITE_URL, '/') . '/api/auth/key', 98 | CURLOPT_POST => 1, 99 | CURLOPT_RETURNTRANSFER => 1, 100 | CURLOPT_HTTPHEADER => array('Content-Type: application/json'), 101 | CURLOPT_POSTFIELDS => json_encode(array( 102 | 'key' => $token, 103 | )) 104 | ); 105 | 106 | curl_setopt_array($curl, $curlOptions); 107 | $curlResponse = json_decode(curl_exec($curl)); 108 | $curlErrorMsg = curl_error($curl); 109 | $curlErrno= curl_errno($curl); 110 | curl_close($curl); 111 | 112 | if ($curlErrorMsg) { 113 | $this->error = $curlErrno . ': ' . $curlErrorMsg . PHP_EOL; 114 | return false; 115 | } 116 | 117 | if (isset($curlResponse->token)) { 118 | $this->jwt = $curlResponse->token; 119 | return true; 120 | } else { 121 | $this->jwt = null; 122 | $this->error = '401: '.$curlResponse->message . PHP_EOL; 123 | return false; 124 | } 125 | } 126 | 127 | private function parseProjectErrors($errors) { 128 | $errorsString = ''; 129 | foreach ($errors as $key => $error) { 130 | $errorsString .= $key . ' => ' . $error . PHP_EOL; 131 | } 132 | return $errorsString; 133 | } 134 | 135 | /** 136 | * Get secrets from secrets file. 137 | * 138 | * @param array $requiredKeys List of keys in secrets file that must exist. 139 | */ 140 | private function get_secrets($requiredKeys) 141 | { 142 | $secretsFile = $_SERVER['HOME'].'/files/private/secrets.json'; 143 | 144 | if (!file_exists($secretsFile)) { 145 | die('No secrets file found. Aborting!'); 146 | } 147 | $secretsContents = file_get_contents($secretsFile); 148 | $secrets = json_decode($secretsContents, 1); 149 | if ($secrets == false) { 150 | die('Could not parse json in secrets file. Aborting!'); 151 | } 152 | 153 | $missing = array_diff($requiredKeys, array_keys($secrets)); 154 | if (!empty($missing)) { 155 | die('Missing required keys in json secrets file: '.implode(',', $missing).'. Aborting!'); 156 | } 157 | 158 | return $secrets; 159 | } 160 | 161 | } 162 | -------------------------------------------------------------------------------- /drush_config_import/README.md: -------------------------------------------------------------------------------- 1 | # Configuration import # 2 | 3 | This example will show you how to integrate Drush commands into your Quicksilver operations, with the practical outcome of importing configuration changes from `.yml` files . You can use the method shown here to run any Drush command you like. 4 | 5 | Note that with the current `webphp` type operations, your timeout is limited to 120 seconds, so long-running operations should be avoided for now. 6 | 7 | ## Instructions ## 8 | 9 | Setting up this example is easy: 10 | 11 | 1. Add the example `drush_config_import.php` script to the 'private/scripts/drush_config_import' directory of your code repository. 12 | 2. Add a Quicksilver operation to your `pantheon.yml` to fire the script before a deploy. 13 | 3. Test a deploy out! 14 | 4. Note that automating this step may not be appropriate for all sites. Sites on which configuration is edited in the live environment may not want to automatically switch to configuration stored in files. For more information, see https://www.drupal.org/documentation/administer/config 15 | 16 | Optionally, you may want to use the `terminus workflow:watch` command to get immediate debugging feedback. 17 | 18 | ### Example `pantheon.yml` ### 19 | 20 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 21 | 22 | ```yaml 23 | api_version: 1 24 | 25 | workflows: 26 | deploy: 27 | after: 28 | - type: webphp 29 | description: Import configuration from .yml files 30 | script: private/scripts/drush_config_import/drush_config_import.php 31 | ``` 32 | -------------------------------------------------------------------------------- /drush_config_import/drush_config_import.php: -------------------------------------------------------------------------------- 1 | devel) && $modules->devel->status !== 'Enabled') { 19 | // This time let's just passthru() to run the drush command so the command output prints to the workflow log. 20 | passthru('drush pm-enable -y devel'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example.pantheon.yml: -------------------------------------------------------------------------------- 1 | --- 2 | api_version: 1 3 | 4 | # PHP Version: 5 | # https://pantheon.io/docs/pantheon-yml#php-version 6 | # Set site's PHP version to 7.0 7 | php_version: 7.0 8 | 9 | # Drush Version 10 | # https://pantheon.io/docs/pantheon-yml/#drush-version 11 | drush_version: 8 12 | 13 | # Protected Web Paths 14 | # https://pantheon.io/docs/pantheon-yml#protected-web-paths 15 | protected_web_paths: 16 | - /example.txt 17 | - /example_directory 18 | 19 | # Nested Docroot 20 | # https://pantheon.io/docs/pantheon-yml#nested-docroot 21 | web_docroot: true 22 | 23 | # Quicksilver Platform Integration Hooks 24 | # https://pantheon.io/docs/pantheon-yml#quicksilver 25 | workflows: 26 | # Site Creation 27 | deploy_product: 28 | after: 29 | - type: webphp 30 | description: Post to Slack after site creation 31 | script: private/scripts/slack_after_site_creation.php 32 | 33 | # Multidev Creation 34 | create_cloud_development_environment: 35 | after: 36 | - type: webphp 37 | description: Post to Slack after Multidev creation 38 | script: private/scripts/slack_after_multidev_creation.php 39 | 40 | # Commits 41 | sync_code: 42 | after: 43 | - type: webphp 44 | description: Post to Slack after each code pushed 45 | script: private/scripts/slack_after_code_push.php 46 | 47 | # Database Clones 48 | clone_database: 49 | before: 50 | - type: webphp 51 | description: Post to Slack before cloning the database 52 | script: private/scripts/slack_before_db_clone.php 53 | after: 54 | - type: webphp 55 | description: sanitize the db after each database Clone 56 | script: private/scripts/sanitize_after_db_clone.php 57 | - type: webphp 58 | description: generate development article content after the database clones 59 | script: private/scripts/generate_dev_content.php 60 | - type: webphp 61 | description: Post to Slack after the database clones 62 | script: private/scripts/slack_after_db_clone.php 63 | 64 | # Code Deploys: Notify, Sanitize (if on test), Post to new relic, update db, and notify completion 65 | deploy: 66 | before: 67 | - type: webphp 68 | description: Post to Slack before cloning the database 69 | script: private/scripts/slack_before_deploy.php 70 | after: 71 | - type: webphp 72 | description: Post to new relic always 73 | script: private/scripts/Post_new_relic.php 74 | - type: webphp 75 | description: sanitize the db after deploy to test 76 | script: private/scripts/sanitize_after_db_clone.php 77 | - type: webphp 78 | description: pull configuration into the database 79 | script: private/scripts/config_pull_after_deploy.php 80 | - type: webphp 81 | description: do a visual regression test with Backtrac.io 82 | script: private/scripts/backtrac_visualregression.php 83 | - type: webphp 84 | description: Post to Slack after each deploy 85 | script: private/scripts/slack_after_deploy.php 86 | 87 | # Cache Clears: Post to Slack after clearing cache 88 | clear_cache: 89 | after: 90 | - type: webphp 91 | description: Post to Slack after cache clear 92 | script: private/scripts/slack_after_clear_cache.php 93 | -------------------------------------------------------------------------------- /generate_dev_content/README.md: -------------------------------------------------------------------------------- 1 | # Automagically Generate Development Content # 2 | 3 | This example will show you how to integrate drush devel generate commands into your quicksilver operations, with the practical outcome of generating development content on each DB clone operation. You can use the method shown here to genereate content of any content type you want. 4 | 5 | ## Instructions ## 6 | 7 | Setting up this example is easy: 8 | 9 | 1. Add the example `generate_dev_content.php` script to the 'private/scripts/' directory of your code repository. 10 | 2. Add a Quicksilver operation to your `pantheon.yml` to fire the script before a deploy. 11 | 3. Test a deploy out! 12 | 13 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. 14 | 15 | ### Example `pantheon.yml` ### 16 | 17 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 18 | 19 | ```yaml 20 | api_version: 1 21 | 22 | workflows: 23 | clone_database: 24 | after: 25 | - type: webphp 26 | description: Generate development article content after the database clones 27 | script: private/scripts/generate_dev_content.php 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /generate_dev_content/generate_dev_content.php: -------------------------------------------------------------------------------- 1 | devel) && isset($modules->devel_generate)) { 12 | 13 | if (isset($modules->devel) && $modules->devel->status !== 'Enabled') { 14 | passthru('drush pm-enable -y devel'); 15 | } 16 | if (isset($modules->devel_generate) && $modules->devel_generate->status !== 'Enabled') { 17 | passthru('drush pm-enable -y devel_generate'); 18 | } 19 | 20 | // Remove the existing production article content 21 | echo "Removing production article content...\n"; 22 | passthru('drush genc --kill --types=article 0 0'); 23 | echo "Removal complete.\n"; 24 | 25 | // Generate new development article content 26 | echo "Generating development article content...\n"; 27 | passthru('drush generate-content 20 --types=article'); 28 | echo "Generation complete.\n"; 29 | 30 | // Disable the Devel and Devel Generate modules as appropriate 31 | if (isset($modules->devel) && $modules->devel->status !== 'Enabled') { 32 | passthru('drush pm-disable -y devel'); 33 | } 34 | if (isset($modules->devel_generate) && $modules->devel_generate->status !== 'Enabled') { 35 | passthru('drush pm-disable -y devel_generate'); 36 | } 37 | } 38 | else { 39 | echo "The Devel and Devel Generate modules must be present for this operation to work"; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /google_chat_notification/README.md: -------------------------------------------------------------------------------- 1 | # Google Chat Integration 2 | 3 | Hook into platform workflows and post notifications to Google Chat. 4 | 5 | ![Google Chat Notification Example](https://user-images.githubusercontent.com/1759794/210021056-07b0afae-0086-40bb-85c1-a96a3d66998d.png) 6 | 7 | ## Instructions 8 | 9 | 1. [Register an Incoming Webhook](https://developers.google.com/chat/how-tos/webhooks) for a Google Chat space. 10 | 2. Copy the secret Webhook URL into a file called `secrets.json` and store it in the [private files](https://pantheon.io/docs/articles/sites/private-files/) directory of each environment where you want to trigger Slack notifications. The secret WebHook URL is like a password, which should not be stored in version control. 11 | 12 | ```shell 13 | $> echo '{"google_chat_webhook": "https://chat.googleapis.com/v1/spaces/AAAAMBwMFRY/messages?key=&token="}' > secrets.json 14 | # Note, you will need to copy the secrets into each environment where you want to trigger Google Chat notifications. 15 | $> `terminus connection:info --field=sftp_command site.env` 16 | Connected to appserver.dev..drush.in. 17 | sftp> cd files 18 | sftp> mkdir private 19 | sftp> cd private 20 | sftp> put secrets.json 21 | sftp> quit 22 | ``` 23 | 24 | 3. Add the example `google_chat_notification.php` script to the `private` directory in the root of your site's codebase, that is under version control. Note this is a different `private` directory than where the `secrets.json` is stored. 25 | 4. Add Quicksilver operations to your `pantheon.yml` 26 | 5. Test a deployment and see the notification in the Google Chat space associated with the webhook. 27 | 28 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. You may also want to customize your notifications further. The [Google Chat API Reference](https://developers.google.com/chat/api/reference/rest/v1/spaces.messages) documentation has more information on options that are available. 29 | 30 | ### Example `pantheon.yml` 31 | 32 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use. Pick and choose the exact workflows that you would like to see notifications for. 33 | 34 | ```yaml 35 | api_version: 1 36 | 37 | workflows: 38 | deploy_product: 39 | after: 40 | - type: webphp 41 | description: Post to Google Chat after site creation 42 | script: private/scripts/google_chat_notification/google_chat_notification.php 43 | create_cloud_development_environment: 44 | after: 45 | - type: webphp 46 | description: Post to Google Chat after Multidev creation 47 | script: private/scripts/google_chat_notification/google_chat_notification.php 48 | deploy: 49 | after: 50 | - type: webphp 51 | description: Post to Google Chat after deploy 52 | script: private/scripts/google_chat_notification/google_chat_notification.php 53 | sync_code: 54 | after: 55 | - type: webphp 56 | description: Post to Google Chat after code commit 57 | script: private/scripts/google_chat_notification/google_chat_notification.php 58 | clear_cache: 59 | after: 60 | - type: webphp 61 | description: Post to Google Chat after cache clear 62 | script: private/scripts/google_chat_notification/google_chat_notification.php 63 | clone_database: 64 | after: 65 | - type: webphp 66 | description: Post to Google Chat after database clone 67 | script: private/scripts/google_chat_notification/google_chat_notification.php 68 | autopilot_vrt: 69 | after: 70 | - type: webphp 71 | description: Post to Google Chat after Autopilot VRT 72 | script: private/scripts/google_chat_notification/google_chat_notification.php 73 | ``` 74 | -------------------------------------------------------------------------------- /google_chat_notification/google_chat_notification.php: -------------------------------------------------------------------------------- 1 | isPantheon() && $this->isQuicksilver()) { 30 | $this->webhook_url = $this->getSecret('google_chat_webhook'); 31 | $this->setQuicksilverVariables(); 32 | 33 | // Get Workflow message 34 | $data = $this->prepareOutputByWorkflow($this->workflow_type); 35 | $this->send($data); 36 | } 37 | } 38 | 39 | /** 40 | * Get the Pantheon site name. 41 | * @return string|null 42 | */ 43 | public function getPantheonSiteName(): ?string 44 | { 45 | return !empty($_ENV['PANTHEON_SITE_NAME']) ? $_ENV['PANTHEON_SITE_NAME'] : NULL; 46 | } 47 | 48 | /** 49 | * Get the Pantheon site id. 50 | * @return string|null 51 | */ 52 | public function getPantheonSiteId(): ?string 53 | { 54 | return !empty($_ENV['PANTHEON_SITE']) ? $_ENV['PANTHEON_SITE'] : NULL; 55 | } 56 | 57 | /** 58 | * Get the Pantheon environment. 59 | * @return string|null 60 | */ 61 | public function getPantheonEnvironment(): ?string 62 | { 63 | return !empty($_ENV['PANTHEON_ENVIRONMENT']) ? $_ENV['PANTHEON_ENVIRONMENT'] : NULL; 64 | } 65 | 66 | /** 67 | * Check if in the Pantheon site context. 68 | * @return bool|void 69 | */ 70 | public function isPantheon() { 71 | if ($this->getPantheonSiteName() !== NULL && $this->getPantheonEnvironment() !== NULL) { 72 | return TRUE; 73 | } 74 | die('No Pantheon environment detected.'); 75 | } 76 | 77 | /** 78 | * Check if in the Quicksilver context. 79 | * @return bool|void 80 | */ 81 | public function isQuicksilver() { 82 | if ($this->isPantheon() && !empty($_POST['wf_type'])) { 83 | return TRUE; 84 | } 85 | die('No Pantheon Quicksilver environment detected.'); 86 | } 87 | 88 | /** 89 | * Set Quicksilver variables from POST data. 90 | * @return void 91 | */ 92 | public function setQuicksilverVariables() { 93 | $this->site_name = $this->getPantheonSiteName(); 94 | $this->site_id = $this->getPantheonSiteId(); 95 | $this->site_env = $this->getPantheonEnvironment(); 96 | $this->user_fullname = $_POST['user_fullname']; 97 | $this->user_email = $_POST['user_email']; 98 | $this->workflow_id = $_POST['trace_id']; 99 | $this->workflow_description = $_POST['wf_description']; 100 | $this->workflow_type = $_POST['wf_type']; 101 | $this->workflow_stage = $_POST['stage']; 102 | $this->workflow = ucfirst($this->workflow_stage) . ' ' . str_replace('_', ' ', $this->workflow_type); 103 | $this->workflow_label = "Quicksilver workflow"; 104 | $this->quicksilver_description = $_POST['qs_description']; 105 | $this->environment_link = "https://$this->site_env-$this->site_name.pantheonsite.io"; 106 | $this->dashboard_link = "https://dashboard.pantheon.io/sites/$this->site_id#$this->site_env"; 107 | } 108 | 109 | /** 110 | * Load secrets from secrets file. 111 | */ 112 | public function getSecrets() 113 | { 114 | if (empty($this->secrets)) { 115 | $secretsFile = $_ENV['HOME'] . 'files/private/secrets.json'; 116 | if (!file_exists($secretsFile)) { 117 | die('No secrets file found. Aborting!'); 118 | } 119 | $secretsContents = file_get_contents($secretsFile); 120 | $secrets = json_decode($secretsContents, TRUE); 121 | if (!$secrets) { 122 | die('Could not parse json in secrets file. Aborting!'); 123 | } 124 | $this->secrets = $secrets; 125 | } 126 | return $this->secrets; 127 | } 128 | 129 | /** 130 | * @param string $key Key in secrets that must exist. 131 | * @return mixed|void 132 | */ 133 | public function getSecret(string $key) { 134 | $secrets = $this->getSecrets(); 135 | $missing = array_diff([$key], array_keys($secrets)); 136 | if (!empty($missing)) { 137 | die('Missing required keys in json secrets file: ' . implode(',', $missing) . '. Aborting!'); 138 | } 139 | return $secrets[$key]; 140 | } 141 | 142 | /** 143 | * @param string $workflow 144 | * @return array|null|string 145 | */ 146 | public function prepareOutputByWorkflow(string $workflow): ?array 147 | { 148 | switch ($workflow) { 149 | case 'sync_code': 150 | $this->workflow_label = "Sync code"; 151 | $output = $this->syncCodeOutput(); 152 | break; 153 | case 'deploy_product': 154 | $this->workflow_label = "Create new site"; 155 | $output = $this->deployProductOutput(); 156 | break; 157 | case 'deploy': 158 | $this->workflow_label = "Code or data deploys targeting an environment"; 159 | $output = $this->deployOutput(); 160 | break; 161 | case 'create_cloud_development_environment': 162 | $this->workflow_label = "Create multidev environment"; 163 | $output = $this->createMultidevOutput(); 164 | break; 165 | case 'clone_database': 166 | $this->workflow_label = "Clone database and files"; 167 | $output = $this->cloneDatabaseOutput(); 168 | break; 169 | case 'clear_cache': 170 | $this->workflow_label = "Clear site cache"; 171 | $output = $this->clearCacheOutput(); 172 | break; 173 | case 'autopilot_vrt': 174 | $this->workflow_label = "Autopilot visual regression test"; 175 | $output = $this->autopilotVrtOutput(); 176 | break; 177 | default: 178 | $output = $this->defaultOutput(); 179 | break; 180 | } 181 | 182 | return $output; 183 | } 184 | 185 | /** 186 | * @param array $cards 187 | * @return array[] 188 | */ 189 | public function createCardPayload(array $cards): array 190 | { 191 | return ['cards_v2' => [(object) $cards]]; 192 | } 193 | 194 | /** 195 | * @param string $id 196 | * @return array 197 | */ 198 | public function createCardTemplate(string $id): array 199 | { 200 | return [ 201 | 'card_id' => $id, 202 | 'card' => [], 203 | ]; 204 | } 205 | 206 | /** 207 | * @param array $buttons 208 | * @return array[][] 209 | */ 210 | public function createCardButtonList(array $buttons): array 211 | { 212 | return [ 213 | 'buttonList' => [ 214 | 'buttons' => $buttons, 215 | ], 216 | ]; 217 | } 218 | 219 | /** 220 | * @param $text 221 | * @param $url 222 | * @return array 223 | */ 224 | public function createCardButton($text, $url): array 225 | { 226 | return [ 227 | 'text' => $text, 228 | 'onClick' => [ 229 | 'openLink' => [ 230 | 'url' => $url, 231 | ], 232 | ], 233 | ]; 234 | } 235 | 236 | /** 237 | * Create common buttons for different workflows. 238 | * @return array 239 | */ 240 | public function createCommonButtons(): array 241 | { 242 | $dashboard_button = $this->createCardButton('View Dashboard', $this->dashboard_link); 243 | $environment_button = $this->createCardButton('View Site Environment', $this->environment_link); 244 | return [$dashboard_button, $environment_button]; 245 | } 246 | 247 | /** 248 | * @return array[] Divider element. 249 | */ 250 | public function createDivider(): array 251 | { 252 | return [ 'divider' => (object) array() ]; 253 | } 254 | 255 | /** 256 | * @param string $text 257 | * @return string[][] 258 | */ 259 | public function createDecoratedText(string $text): array 260 | { 261 | return [ 262 | 'decoratedText' => [ 263 | 'text' => $text, 264 | ] 265 | ]; 266 | } 267 | 268 | /** 269 | * @param $title 270 | * @param $subtitle 271 | * @param string $image_url 272 | * @param string $image_type 273 | * @return array 274 | */ 275 | public function createCardHeader($title, $subtitle = null, string $image_url = "https://avatars.githubusercontent.com/u/88005016", string $image_type = "CIRCLE" ): array 276 | { 277 | return [ 278 | 'title' => $title, 279 | 'subtitle' => $subtitle, 280 | 'imageUrl' => $image_url, 281 | 'imageType' => $image_type, 282 | ]; 283 | } 284 | 285 | /** 286 | * @param string $messageText 287 | * @param array $buttons 288 | * @return array 289 | */ 290 | public function prepareCardOutput(string $messageText, array $buttons = []): array 291 | { 292 | $cardTemplate = $this->createCardTemplate($this->workflow_id); 293 | $cardTemplate['card']['header'] = $this->createCardHeader($this->quicksilver_description, $this->workflow_label); 294 | 295 | // Create card widgets. 296 | $widgets = []; 297 | $widgets[] = $this->createDecoratedText(trim($messageText)); 298 | if (!empty($buttons)) { 299 | $widgets[] = $this->createDivider(); 300 | $widgets[] = $this->createCardButtonList($buttons); 301 | } 302 | 303 | $cardTemplate['card']['sections'] = ['widgets' => $widgets]; 304 | 305 | return $cardTemplate; 306 | } 307 | 308 | /** 309 | * @param array $post 310 | * @return void 311 | */ 312 | public function send(array $post) { 313 | 314 | $payload = json_encode($post, JSON_PRETTY_PRINT); 315 | 316 | print_r($payload); 317 | 318 | $ch = curl_init(); 319 | curl_setopt($ch, CURLOPT_URL, $this->webhook_url); 320 | curl_setopt($ch, CURLOPT_POST, 1); 321 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 322 | curl_setopt($ch, CURLOPT_TIMEOUT, 5); 323 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); 324 | curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); 325 | 326 | // Watch for messages with `terminus workflows watch --site=SITENAME` 327 | print("\n==== Posting to Google Chat ====\n"); 328 | $result = curl_exec($ch); 329 | print("RESULT: $result"); 330 | // $payload_pretty = json_encode($post,JSON_PRETTY_PRINT); // Uncomment to debug JSON 331 | // print("JSON: $payload_pretty"); // Uncomment to Debug JSON 332 | print("\n===== Post Complete! =====\n"); 333 | curl_close($ch); 334 | } 335 | 336 | /** 337 | * Get output from shell commands. 338 | * @param $cmd 339 | * @return string 340 | */ 341 | public function getCommandOutput($cmd): string 342 | { 343 | $command = escapeshellcmd($cmd); 344 | return trim(shell_exec($command)); 345 | } 346 | 347 | /** 348 | * Generate message for sync_code workflow. 349 | * @return array[] 350 | */ 351 | public function syncCodeOutput(): array 352 | { 353 | // Get the committer, hash, and message for the most recent commit. 354 | $committer = $this->getCommandOutput('git log -1 --pretty=%cn'); 355 | $email = $this->getCommandOutput('git log -1 --pretty=%ce'); 356 | $message = $this->getCommandOutput('git log -1 --pretty=%B'); 357 | $hash = $this->getCommandOutput('git log -1 --pretty=%h'); 358 | 359 | $text = <<Committer: $committer ($email) 361 | Commit: $hash 362 | Commit Message: $message 363 | MSG; 364 | 365 | $card = $this->prepareCardOutput($text, $this->createCommonButtons()); 366 | return $this->createCardPayload($card); 367 | 368 | } 369 | 370 | /** 371 | * Generate message for deploy_product workflow. 372 | * @return array[]|object[][] 373 | */ 374 | public function deployProductOutput(): array 375 | { 376 | $text = <<Site name: $this->site_name 379 | Created by: $this->user_email 380 | MSG; 381 | 382 | $card = $this->prepareCardOutput($text, $this->createCommonButtons()); 383 | return $this->createCardPayload($card); 384 | } 385 | 386 | /** 387 | * Generate message for deploy workflow. 388 | * @return array[]|object[][] 389 | */ 390 | public function deployOutput(): array 391 | { 392 | // Find out what tag we are on and get the annotation. 393 | $deploy_tag = $this->getCommandOutput('git describe --tags'); 394 | $deploy_message = $_POST['deploy_message']; 395 | 396 | $text = <<site_env environment of $this->site_name by $this->user_email is complete! 398 | 399 | Deploy Tag: $deploy_tag 400 | Deploy Note: $deploy_message 401 | MSG; 402 | 403 | $card = $this->prepareCardOutput($text, $this->createCommonButtons()); 404 | return $this->createCardPayload($card); 405 | } 406 | 407 | /** 408 | * Generate message for create_cloud_development_environment workflow. 409 | * @return array[]|object[][] 410 | */ 411 | public function createMultidevOutput(): array 412 | { 413 | $text = <<Environment name: $this->site_env 416 | Created by: $this->user_email 417 | MSG; 418 | 419 | $card = $this->prepareCardOutput($text, $this->createCommonButtons()); 420 | return $this->createCardPayload($card); 421 | } 422 | 423 | /** 424 | * Generate message for clone_database workflow. 425 | * @return array[]|object[][] 426 | */ 427 | public function cloneDatabaseOutput(): array 428 | { 429 | $to = $_POST['to_environment']; 430 | $from = $_POST['from_environment']; 431 | $text = <<From environment: $from 433 | To environment: $to 434 | Started by: $this->user_email 435 | MSG; 436 | 437 | $card = $this->prepareCardOutput($text, $this->createCommonButtons()); 438 | return $this->createCardPayload($card); 439 | } 440 | 441 | /** 442 | * Generate message for clear_cache workflow. 443 | * @return array[]|object[][] 444 | */ 445 | public function clearCacheOutput(): array 446 | { 447 | $text = "Cleared caches on the $this->site_env environment of $this->site_name!"; 448 | $card = $this->prepareCardOutput($text); 449 | return $this->createCardPayload($card); 450 | } 451 | 452 | /** 453 | * Generate message for autopilot_vrt workflow. 454 | * @return array[]|object[][] 455 | */ 456 | public function autopilotVrtOutput(): array 457 | { 458 | $status = $_POST['vrt_status']; 459 | $result_url = $_POST['vrt_result_url']; 460 | $updates_info = $_POST['updates_info']; 461 | $text = <<Status: $status 463 | Report: View VRT Report 464 | Started by: $this->user_email 465 | MSG; 466 | 467 | $card = $this->prepareCardOutput($text, $this->createCommonButtons()); 468 | return $this->createCardPayload($card); 469 | } 470 | 471 | /** 472 | * Generate default output for undefined workflows. 473 | * @return array 474 | */ 475 | public function defaultOutput(): array 476 | { 477 | return ['text' => $this->quicksilver_description ]; 478 | } 479 | 480 | } 481 | -------------------------------------------------------------------------------- /jenkins/README.md: -------------------------------------------------------------------------------- 1 | # Jenkins Integration # 2 | 3 | This script shows how easy it is to integrate Jenkins builds from your Pantheon project using Quicksilver. As a bonus, we also show you how to manage API keys/User Data outside of your site repository. 4 | 5 | ## Instructions ## 6 | 7 | Setting up this example is easy: 8 | 9 | First, configure a Jenkins Job and configure it with a token at https://YOUR_JENKINS_SERVER_ADDRESS/job/JOB_NAME/configure . Found under "Build Triggers" tab, check the "Trigger Builds remotely" checkbox and enter a TOKEN_VALUE. 10 | 11 | Copy the following information into a secrets.json file: 12 | - jenkins_url: JENKINS_WEBHOOK_URL (https://YOUR_JENKINS_SERVER_ADDRESS/job/JOB_NAME/build) 13 | - token: TOKEN_VALUE (Setup above) 14 | - username: USERNAME (Your Jenkins Username) 15 | - api_token: API_TOKEN (Found at https://YOUR_JENKINS_SERVER_ADDRESS/YOUR_USERNAME/configure under API Token) 16 | 17 | For example: 18 | 19 | ```shell 20 | $> echo '{"jenkins_url": "JENKINS_WEBHOOK_URL","token": "TOKEN_VALUE","username": "USERNAME","api_token": "API_TOKEN"}' > secrets.json 21 | $> `terminus site connection-info --env=dev --site=your-site --field=sftp_command` 22 | Connected to appserver.xxx.xxxxxx-xxxx-xxxx-xxxxxx.drush.in. 23 | sftp> cd files 24 | sftp> mkdir private 25 | sftp> cd private 26 | sftp> put secrets.json 27 | 28 | ``` 29 | 30 | - Add the example `jenkins_integration.php` script to the `private` directory of your code repository. 31 | - Add a Quicksilver operation to your `pantheon.yml` to fire the script a deploy. 32 | - Test a deploy out! 33 | 34 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. 35 | 36 | You may also want to record in Jenkins why you are triggering that particular build. You can optionally append '&cause=Cause+Text' to the post data if you want that included in the build records. 37 | 38 | ### Example `pantheon.yml` ### 39 | 40 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 41 | 42 | ```yaml 43 | api_version: 1 44 | 45 | workflows: 46 | deploy: 47 | after: 48 | - type: webphp 49 | description: Integrate With Jenkins 50 | script: private/scripts/jenkins_integration.php 51 | ``` 52 | -------------------------------------------------------------------------------- /jenkins/example.secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "jenkins_url": "https://YOUR_JENKINS_SERVER_ADDRESS/job/JOB_NAME/build", 3 | "token:": "TOKEN_VALUE", 4 | "username": "USERNAME", 5 | "api_token": "API_TOKEN" 6 | } 7 | -------------------------------------------------------------------------------- /jenkins/jenkins_integration.php: -------------------------------------------------------------------------------- 1 | $secrets->token, 19 | )); 20 | 21 | //Execute the request 22 | $response = curl_exec($curl); 23 | 24 | // TODO: could produce some richer responses here. 25 | // Could even chain this to a slack notification. It's up to you! 26 | if ($response) { 27 | echo "Build Queued"; 28 | } 29 | else { 30 | echo "Build Failed"; 31 | } 32 | 33 | 34 | /** 35 | * Get secrets from secrets file. 36 | * 37 | * @param string $file path within files/private that has your json 38 | */ 39 | function _get_secrets($file) 40 | { 41 | $secrets_file = $_SERVER['HOME'] . '/files/private/' . $file; 42 | if (!file_exists($secrets_file)) { 43 | die('No secrets file found. Aborting!'); 44 | } 45 | $secrets_json = file_get_contents($secrets_file); 46 | $secrets = json_decode($secrets_json, 1); 47 | if ($secrets == false) { 48 | die('Could not parse json in secrets file. Aborting!'); 49 | } 50 | return $secrets; 51 | } -------------------------------------------------------------------------------- /jira_integration/README.md: -------------------------------------------------------------------------------- 1 | # Jira Integration # 2 | 3 | This example parses commit messages for Jira issue IDs and adds the commit message as a comment in the related Jira issue. 4 | 5 | Example comments: 6 | 7 | MYPROJECT-9: Adjust layout spacing. 8 | Fixes issues MYPROJECT-4 and MYPROJECT-7. 9 | 10 | Commits that contain multiple Jira issues will post comments to each issue mentioned. A comment will be added each time a commit is pushed to any dev or multidev branch; each Jira comment is labeled with the appropriate commit hash and Pantheon environment that triggered the post. 11 | 12 | ## Instructions ## 13 | 14 | - Copy your Jira credentials and the URL to your Jira instance into a file called `secrets.json` and store it in the private files area of your site 15 | 16 | ```shell 17 | $> php -r "print json_encode(['jira_url'=>'https://myjira.atlassian.net','jira_user'=>'serviceaccount','jira_pass'=>'secret']);" > secrets.json 18 | # Note, you'll need to copy the secrets into each environment where you want to save commit messages to Jira 19 | $> `terminus site connection-info --env=dev --site=your-site --field=sftp_command` 20 | Connected to appserver.dev.d1ef01f8-364c-4b91-a8e4-f2a46f14237e.drush.in. 21 | sftp> cd files 22 | sftp> mkdir private 23 | sftp> cd private 24 | sftp> put secrets.json 25 | 26 | ``` 27 | - Add the example `jira_integration.php` script to the `private` directory of your code repository. 28 | - Add a Quicksilver operation to your `pantheon.yml` to fire the script after a deploy. 29 | - Push code with a commit message containing a Jira issue ID! 30 | 31 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. 32 | 33 | ### Example `pantheon.yml` ### 34 | 35 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 36 | 37 | ```yaml 38 | api_version: 1 39 | 40 | workflows: 41 | sync_code: 42 | after: 43 | - type: webphp 44 | description: Jira Integration 45 | script: private/scripts/jira_integration.php 46 | ``` 47 | -------------------------------------------------------------------------------- /jira_integration/jira_integration.php: -------------------------------------------------------------------------------- 1 | /dev/null", $output, $status); 26 | if (!$status) { 27 | $last_commithash = $last_processed_commithash; 28 | } 29 | } 30 | // Update the last commit file with the latest commit 31 | file_put_contents($commit_file, $current_commithash, LOCK_EX); 32 | 33 | // Retrieve git log for commits after last processed, to current 34 | $commits = _get_commits($current_commithash, $last_commithash, $env); 35 | 36 | // Check each commit message for Jira ticket numbers 37 | foreach ($commits['jira'] as $ticket_id => $commit_ids) { 38 | foreach ($commit_ids as $commit_id) { 39 | send_commit($secrets, $ticket_id, $commits['history'][$commit_id]); 40 | } 41 | } 42 | 43 | /** 44 | * Do git operations to find all commits between the specified commit hashes, 45 | * and return an associative array containing all applicable commits that 46 | * contain references to Jira issues. 47 | */ 48 | function _get_commits($current_commithash, $last_commithash, $env) { 49 | $commits = array( 50 | // Raw output of git log since the last processed 51 | 'history_raw' => null, 52 | // Formatted array of commits being sent to jira 53 | 'history' => array(), 54 | // An array keyed by jira ticket id, each holding an 55 | // array of commit ids. 56 | 'jira' => array() 57 | ); 58 | 59 | $cmd = 'git log'; // add -p to include diff 60 | if (!$last_commithash) { 61 | $cmd .= ' -n 1'; 62 | } 63 | else { 64 | $cmd .= ' ' . $last_commithash . '...' . $current_commithash; 65 | } 66 | $commits['history_raw'] = shell_exec($cmd); 67 | // Parse raw history into an array of commits 68 | $history = preg_split('/^commit /m', $commits['history_raw'], -1, PREG_SPLIT_NO_EMPTY); 69 | foreach ($history as $str) { 70 | $commit = array( 71 | 'full' => 'Commit: ' . $str 72 | ); 73 | // Only interested in the lines before the diff now 74 | $lines = explode("\n", $str); 75 | $commit['id'] = $lines[0]; 76 | $commit['message'] = trim(implode("\n", array_slice($lines, 4))); 77 | $commit['formatted'] = '{panel:title=Commit: ' . substr($commit['id'], 0, 10) . ' [' . $env . ']|borderStyle=dashed|borderColor=#ccc|titleBGColor=#e5f2ff|bgColor=#f2f2f2} 78 | ' . $commit['message'] . ' 79 | ~' . $lines[1] . ' - ' . $lines[2] . '~ 80 | {panel}'; 81 | // Look for matches on a Jira issue ID format 82 | // Expected pattern: "PROJECT-ID: comment". 83 | preg_match('/([A-Z]+-[0-9]+)/i', $commit['message'], $matches); 84 | if (count($matches) > 0) { 85 | // Build the $commits['jira'] array so there is 86 | // only 1 item per ticket id 87 | foreach ($matches as $ticket_id) { 88 | $ticket_id = strtoupper($ticket_id); 89 | if (!isset($commits['jira'][$ticket_id])) { 90 | $commits['jira'][$ticket_id] = array(); 91 | } 92 | // ... and only 1 item per commit id 93 | $commits['jira'][$ticket_id][$commit['id']] = $commit['id']; 94 | } 95 | // Add the commit to the history array since there was a match. 96 | $commits['history'][$commit['id']] = $commit; 97 | } 98 | } 99 | return $commits; 100 | } 101 | 102 | /** 103 | * Send commits to Jira 104 | */ 105 | function send_commit($secrets, $ticket_id, $commit) { 106 | $payload = json_encode(array('body' => $commit['formatted'])); 107 | $ch = curl_init(); 108 | curl_setopt($ch, CURLOPT_URL, $secrets['jira_url'] . '/rest/api/2/issue/' . $ticket_id . '/comment'); 109 | curl_setopt($ch, CURLOPT_USERPWD, $secrets['jira_user'] . ':' . $secrets['jira_pass']); 110 | curl_setopt($ch, CURLOPT_POST, 1); 111 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 112 | curl_setopt($ch, CURLOPT_TIMEOUT, 5); 113 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json')); 114 | curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); 115 | print("\n==== Posting to Jira ====\n"); 116 | $result = curl_exec($ch); 117 | print("RESULT: $result"); 118 | print("\n===== Post Complete! =====\n"); 119 | curl_close($ch); 120 | } 121 | 122 | /** 123 | * Get secrets from secrets file. 124 | * 125 | * @param array $requiredKeys List of keys in secrets file that must exist. 126 | */ 127 | function _get_secrets($requiredKeys, $defaults) 128 | { 129 | $secretsFile = $_SERVER['HOME'] . '/files/private/secrets.json'; 130 | if (!file_exists($secretsFile)) { 131 | die('No secrets file found. Aborting!'); 132 | } 133 | $secretsContents = file_get_contents($secretsFile); 134 | $secrets = json_decode($secretsContents, 1); 135 | if ($secrets == false) { 136 | die('Could not parse json in secrets file. Aborting!'); 137 | } 138 | $secrets += $defaults; 139 | $missing = array_diff($requiredKeys, array_keys($secrets)); 140 | if (!empty($missing)) { 141 | die('Missing required keys in json secrets file: ' . implode(',', $missing) . '. Aborting!'); 142 | } 143 | return $secrets; 144 | } 145 | -------------------------------------------------------------------------------- /new_relic_apdex_t/README.md: -------------------------------------------------------------------------------- 1 | # Set New Relic Apdex T values on Multidev Creation # 2 | 3 | 4 | [All sites on Pantheon include access to New Relic APM Pro](https://pantheon.io/features/new-relic). This application performance monitoring relies on the site owners setting time benchmark to measure against. Ideally, your Drupal or WordPress responds quickly to requests. And if the site is not responding quickly, New Relic can alert you. 5 | 6 | The question is, where do you want to set the bar? By default, New Relic uses 0.5 as the target number of seconds. This value is called "T" in the [Apdex (Application Performance Index) formula](https://docs.newrelic.com/docs/apm/new-relic-apm/apdex/apdex-measuring-user-satisfaction). 7 | 8 | In addition to monitoring how fast the server (Drupal or WordPress) respond, New Relic can monitor how fast real world browsers render your site. Browser performance is measured with the same Apdex formula. By default, New Relic uses a much more generous 7 seconds as the T value in browser Apdex. 9 | 10 | We recommend that any team working on a site discuss expectations for server-side and client-side performance and set T values accordingly. As you are developing new features with [Pantheon Multidev,](https://pantheon.io/features/multidev-cloud-environments) you might even want the Multidev environments to have more stringent T values than Test or Live environments. 11 | 12 | This Quicksilver example shows how you can set custom T values for Multidev environments when they are created. Otherwise each environment will use the default values of 0.5 and 7 for server and browser respectively. 13 | 14 | To do the actual setting of default values this script first gets an API key and then uses that key to interact with New Relic's REST API to set a T values based on the existing values from the dev (or test/live) environment. 15 | 16 | ## Instructions ## 17 | 18 | To use this example: 19 | 20 | 1. [Activate New Relic Pro](https://pantheon.io/docs/new-relic/#activate-new-relic-pro) within your site dashboard. 21 | 2. Get a [New Relic User Key](https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/) 22 | 3. Using [Terminus Secrets Manager Plugin](https://github.com/pantheon-systems/terminus-secrets-manager-plugin), set a site secret for the API key just created (e.g. `new_relic_api_key`, if you name it something else, make sure to update in the script below). Make sure type is `runtime` and scope contains `web`. 23 | ``` 24 | terminus secret:site:set mysite new_relic_api_key --scope=web --type=runtime MY_API_KEY_HERE 25 | ``` 26 | 4. Add the example `new_relic_apdex_t.php` script to the `private/scripts` directory of your code repository. 27 | 5. Optionally, modify the environment to pull existing threshold T values from at the top of the file. This defaults to `dev` but can also be `test` or `live`. 28 | 29 | ```php 30 | // get New Relic info from the dev environment 31 | // Change to test or live as you wish 32 | $app_info = get_app_info( 'dev' ); 33 | 34 | ``` 35 | 36 | 4. Add a Quicksilver operation to your `pantheon.yml` to fire the script after a deploy. (One gotcha is that this script cannot be the first or only script called as part of Multidev creation. Before the New Relic API recognizes the the Multidev environment, that environment needs to have received at least one previous request.) 37 | 38 | 39 | ### Example `pantheon.yml` ### 40 | 41 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 42 | 43 | ```yaml 44 | api_version: 1 45 | 46 | workflows: 47 | create_cloud_development_environment: 48 | after: 49 | # The setting of Apdex cannot be the first traffic the new Multidev environment 50 | # receives. A New Relic application ID is not available until after the 51 | # environment receives some traffic. So run another script prior to calling 52 | # new_relic_apdex_t.php. In this case drush_config_import.php is an 53 | # arbitrary example. 54 | - type: webphp 55 | description: Drush Config Import 56 | script: private/scripts/drush_config_import.php 57 | - type: webphp 58 | description: Set Apdex T values 59 | script: private/scripts/new_relic_apdex_t.php 60 | ``` 61 | -------------------------------------------------------------------------------- /new_relic_apdex_t/new_relic_apdex_t.php: -------------------------------------------------------------------------------- 1 | [ 135 | 'name' => $app_name, 136 | 'settings' => [ 137 | 'app_apdex_threshold' => $app_apdex_threshold, 138 | 'end_user_apdex_threshold' => $end_user_apdex_threshold, 139 | 'enable_real_user_monitoring' => $enable_real_user_monitoring, 140 | ], 141 | ], 142 | ]; 143 | 144 | $data_json = json_encode( $settings ); 145 | 146 | $ch = curl_init(); 147 | curl_setopt( $ch, CURLOPT_URL, $url ); 148 | curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); 149 | curl_setopt( $ch, CURLOPT_POSTFIELDS, $data_json ); 150 | curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, "PUT" ); 151 | $headers = [ 152 | 'X-API-KEY:' . $api_key, 153 | 'Content-Type: application/json' 154 | ]; 155 | curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); 156 | $result = curl_exec( $ch ); 157 | if ( curl_errno( $ch ) ) { 158 | echo 'Error:' . curl_error( $ch ); 159 | } 160 | curl_close( $ch ); 161 | 162 | echo "===== Finished Setting New Relic Values =====\n"; 163 | } 164 | -------------------------------------------------------------------------------- /new_relic_deploy/README.md: -------------------------------------------------------------------------------- 1 | # New Relic Deploy Logs # 2 | 3 | This example will show you how you can automatically log changes to your site into [New Relic's Deployments Page](https://docs.newrelic.com/docs/apm/applications-menu/events/deployments-page) when the workflow fires on Pantheon. This can be quite useful for keeping track of all your performance improvements! 4 | 5 | > **Note:** This example will work for all Pantheon sites once the bundled [New Relic APM Pro feature](https://pantheon.io/features/new-relic) is activated, regardless of service level. 6 | 7 | ## Instructions ## 8 | 9 | Setting up this example is easy: 10 | 11 | 1. [Activate New Relic Pro](https://pantheon.io/docs/new-relic/#activate-new-relic-pro) within your site Dashboard. 12 | 2. Get a [New Relic User Key](https://one.newrelic.com/launcher/api-keys-ui.api-keys-launcher) 13 | 3. Using [Terminus Secrets Manager Plugin](https://github.com/pantheon-systems/terminus-secrets-manager-plugin), set a site secret for the API key just created (e.g. `new_relic_api_key`, if you name it something else, make sure to update in the Terminus command below, and within the `new_relic_deploy.php` script in Step 4). Make sure type is `runtime` and scope contains `web`. 14 | ``` 15 | terminus secret:site:set mysite new_relic_api_key --scope=web --type=runtime MY_API_KEY_HERE 16 | ``` 17 | 4. Add the example `new_relic_deploy.php` script to the `/private/scripts/` directory of your code repository. 18 | 5. Add a Quicksilver operation to your `pantheon.yml` to fire the script after a deploy. 19 | 6. Test a deploy out! 20 | 21 | Optionally, you may want to use the `terminus workflow:watch yoursitename` command to get immediate debugging feedback. 22 | 23 | ### Example `pantheon.yml` ### 24 | 25 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 26 | 27 | ```yaml 28 | # Always need to specify the pantheon.yml API version. 29 | api_version: 1 30 | 31 | # You might also want some of the following here: 32 | # php_version: 7.0 33 | # drush_version: 8 34 | 35 | workflows: 36 | # Log to New Relic when deploying to test or live. 37 | deploy: 38 | after: 39 | - type: webphp 40 | description: Log to New Relic 41 | script: private/scripts/new_relic_deploy.php 42 | # Also log sync_code so you can track new code going into dev/multidev. 43 | sync_code: 44 | after: 45 | - type: webphp 46 | description: Log to New Relic 47 | script: private/scripts/new_relic_deploy.php 48 | 49 | ``` 50 | -------------------------------------------------------------------------------- /new_relic_deploy/new_relic_deploy.php: -------------------------------------------------------------------------------- 1 | [ 64 | "revision" => $revision, 65 | "changelog" => $changelog, 66 | "description" => $description, 67 | "user" => $user, 68 | ] 69 | ]; 70 | 71 | echo "Logging deployment in New Relic App $app_guid...\n"; 72 | $response = create_newrelic_deployment_change_tracking($data['api_key'], $app_guid, $user, $revision, $changelog, $description); 73 | 74 | echo "\nResponse from New Relic:" . $response; 75 | 76 | echo "\nDone!\n"; 77 | 78 | /** 79 | * Gets the New Relic API Key so that further requests can be made. 80 | * 81 | * Also gets New Relic's name for the given environment. 82 | */ 83 | function get_nr_connection_info() { 84 | $output = array(); 85 | 86 | $output['app_name'] = ini_get('newrelic.appname'); 87 | if (function_exists('pantheon_get_secret')) { 88 | $output['api_key'] = pantheon_get_secret(API_KEY_SECRET_NAME); 89 | } 90 | 91 | return $output; 92 | } 93 | 94 | // Get GUID of the current environment. 95 | function get_app_guid(string $api_key, string $app_name): string { 96 | $url = 'https://api.newrelic.com/graphql'; 97 | $headers = ['Content-Type: application/json', 'API-Key: ' . $api_key]; 98 | 99 | // Updated entitySearch query with name filter 100 | $data = '{ "query": "{ actor { entitySearch(query: \\"(domain = \'APM\' and type = \'APPLICATION\' and name = \'' . $app_name . '\')\\") { count query results { entities { entityType name guid } } } } }" }'; 101 | 102 | $ch = curl_init(); 103 | curl_setopt($ch, CURLOPT_URL, $url); 104 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 105 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 106 | curl_setopt($ch, CURLOPT_POST, 1); 107 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 108 | 109 | $response = curl_exec($ch); 110 | curl_close($ch); 111 | 112 | 113 | $decoded_response = json_decode($response, true); 114 | 115 | // Error handling for API response. 116 | if (isset($decoded_response['errors'])) { 117 | echo "Error: " . $decoded_response['errors'][0]['message'] . "\n"; 118 | return ''; 119 | } 120 | if (!isset($decoded_response['data']['actor']['entitySearch']['results']['entities']) || !is_array($decoded_response['data']['actor']['entitySearch']['results']['entities'])) { 121 | echo "Error: No entities found in New Relic response\n"; 122 | return ''; 123 | } 124 | 125 | $entities = $decoded_response['data']['actor']['entitySearch']['results']['entities']; 126 | // Since we filtered by name, the first entity should be the correct one. 127 | if (isset($entities[0]['guid'])) { 128 | return $entities[0]['guid']; 129 | } 130 | return ''; 131 | } 132 | 133 | function create_newrelic_deployment_change_tracking(string $api_key, string $entityGuid, string $user, string $version, string $changelog, string $description): string { 134 | $url = 'https://api.newrelic.com/graphql'; 135 | $headers = ['Content-Type: application/json', 'API-Key: ' . $api_key]; 136 | 137 | $timestamp = round(microtime(true) * 1000); 138 | 139 | // Construct the mutation with dynamic variables 140 | $data = '{ 141 | "query": "mutation { changeTrackingCreateDeployment(deployment: { version: \\"' . $version . '\\" user: \\"' . $user . '\\" timestamp: ' . $timestamp . ' entityGuid: \\"' . $entityGuid . '\\" description: \\"' . $description . '\\" changelog: \\"' . $changelog . '\\" }) { changelog deploymentId description entityGuid timestamp user version } }" 142 | }'; 143 | 144 | $ch = curl_init(); 145 | 146 | curl_setopt($ch, CURLOPT_URL, $url); 147 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 148 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 149 | curl_setopt($ch, CURLOPT_POST, 1); 150 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 151 | 152 | $response = curl_exec($ch); 153 | curl_close($ch); 154 | return $response; 155 | } 156 | -------------------------------------------------------------------------------- /new_relic_monitor/README.md: -------------------------------------------------------------------------------- 1 | # New Relic Monitor # 2 | 3 | This example will show you how you can automatically create a [New Relic Synthetics Ping Monitor](https://docs.newrelic.com/docs/synthetics/new-relic-synthetics/getting-started/types-synthetics-monitors) when a live deployment is triggered on Pantheon. This can be useful for monitoring the server response time and uptime from various locations around the world. 4 | 5 | This script uses the `pantheon_curl()` command to fetch the extended metadata information for the site/environment, which includes the New Relic API key. Using New Relic's REST API, we first check to see if the monitor exists, and if not, we will create a new one. 6 | 7 | > **Note:** This example will work for all Pantheon sites (except Basic) once the bundled [New Relic APM Pro feature](https://pantheon.io/features/new-relic) is activated. 8 | 9 | ## Instructions ## 10 | 11 | Setting up this example is easy: 12 | 13 | 1. [Activate New Relic Pro](https://pantheon.io/docs/new-relic/#activate-new-relic-pro) within your site Dashboard. 14 | 2. Get a [New Relic User Key](https://docs.newrelic.com/docs/apis/intro-apis/new-relic-api-keys/) 15 | 3. Using [Terminus Secrets Manager Plugin](https://github.com/pantheon-systems/terminus-secrets-manager-plugin), set a site secret for the API key just created (e.g. `new_relic_api_key`, if you name it something else, make sure to update in the script below). Make sure type is `runtime` and scope contains `web`. 16 | ``` 17 | terminus secret:site:set mysite new_relic_api_key --scope=web --type=runtime MY_API_KEY_HERE 18 | ``` 19 | 4. Add the example `new_relic_monitor.php` script to the `private` directory of your code repository. 20 | 5. Add a Quicksilver operation to your `pantheon.yml` to fire the script after a deploy. 21 | 6. Test a deploy out! 22 | 23 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. 24 | 25 | ### Example `pantheon.yml` ### 26 | 27 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 28 | 29 | ```yaml 30 | # Always need to specify the pantheon.yml API version. 31 | api_version: 1 32 | 33 | workflows: 34 | # Create a New Relic Monitor when deploying to live. 35 | deploy: 36 | after: 37 | - type: webphp 38 | description: Log to New Relic 39 | script: private/scripts/new_relic_monitor.php 40 | ``` 41 | -------------------------------------------------------------------------------- /new_relic_monitor/new_relic_monitor.php: -------------------------------------------------------------------------------- 1 | createMonitor(); 19 | } else { 20 | die("\n\nAborting: Only run on live deployments."); 21 | } 22 | echo "Done!"; 23 | 24 | /** 25 | * Basic class for New Relic calls. 26 | */ 27 | class NewRelic { 28 | 29 | private $nr_app_name; // New Relic account info. 30 | private $api_key; // New Relic Admin Key 31 | public $site_uri; // Pantheon Site URI 32 | 33 | /** 34 | * Initialize class 35 | * 36 | * @param [string] $api_key New Relic Admin API key. 37 | */ 38 | function __construct() { 39 | $this->site_uri = 'https://' . $_ENV['PANTHEON_ENVIRONMENT'] . '-' . $_ENV['PANTHEON_SITE_NAME'] . '.pantheonsite.io'; 40 | 41 | if (function_exists('pantheon_get_secret')) { 42 | $this->api_key = pantheon_get_secret(API_KEY_SECRET_NAME); 43 | } 44 | 45 | $this->nr_app_name = ini_get('newrelic.appname'); 46 | 47 | // Fail fast if we're not going to be able to call New Relic. 48 | if ($this->api_key == false) { 49 | die("\n\nALERT! No New Relic API key could be found.\n\n"); 50 | } 51 | } 52 | 53 | /** 54 | * Get a list of monitors. 55 | * 56 | * @return [array] $data 57 | */ 58 | public function getMonitors() { 59 | $data = $this->curl('https://synthetics.newrelic.com/synthetics/api/v3/monitors?limit=50'); 60 | return $data; 61 | } 62 | 63 | /** 64 | * Get a list of ping locations. 65 | * 66 | * @return [array] $data 67 | */ 68 | public function getLocations() { 69 | $data = $this->curl('https://synthetics.newrelic.com/synthetics/api/v1/locations'); 70 | return $data; 71 | } 72 | 73 | /** 74 | * Validate if monitor for current environment already exists. 75 | * 76 | * @param [string] $id 77 | * @return boolean 78 | * 79 | * @todo Finish validating after getting Pro API key. 80 | */ 81 | public function validateMonitor($id) { 82 | $monitors = $this->getMonitors(); 83 | return in_array($id, $monitors['name']); 84 | } 85 | 86 | /** 87 | * Create a new ping monitor. 88 | * 89 | * @param integer $freq The frequency of pings in seconds. 90 | * @return void 91 | */ 92 | public function createMonitor($freq = 60) { 93 | 94 | // List of locations. 95 | $locations = $this->getLocations(); 96 | 97 | $body = [ 98 | 'name' => $this->nr_app_name, 99 | 'type' => 'SIMPLE', 100 | 'frequency' => $freq, 101 | 'uri' => $this->site_uri, 102 | 'locations' => [], 103 | 'status' => 'ENABLED', 104 | ]; 105 | 106 | $req = $this->curl('https://synthetics.newrelic.com/synthetics/api/v3/monitors', [], $body, 'POST'); 107 | print_r($req); 108 | } 109 | 110 | /** 111 | * Custom curl function for reusability. 112 | */ 113 | public function curl($url, $headers = [], $body = [], $method = 'GET') { 114 | // Initialize Curl. 115 | $ch = curl_init(); 116 | 117 | // Include NR API key 118 | $headers[] = 'X-Api-Key: ' . $this->api_key; 119 | 120 | // Add POST body if method requires. 121 | if ($method == 'POST') { 122 | $payload = json_encode($body); 123 | $headers[] = 'Content-Type: application/json'; 124 | curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); 125 | } 126 | 127 | curl_setopt($ch, CURLOPT_URL, $url); 128 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 129 | curl_setopt($ch, CURLOPT_TIMEOUT, 5); 130 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); 131 | 132 | $result = curl_exec($ch); 133 | curl_close($ch); 134 | 135 | // print("JSON: " . json_encode($post,JSON_PRETTY_PRINT)); // Uncomment to debug JSON 136 | return $result; 137 | } 138 | } -------------------------------------------------------------------------------- /pivotal-tracker/README.md: -------------------------------------------------------------------------------- 1 | # Pivotal Tracker Integration # 2 | 3 | This example parses commit messages for Pivotal Tracker story IDs and adds the commit message as an activity to the story. 4 | 5 | Example comments: 6 | 7 | ```shell 8 | [#148528125] Making a change to this story. 9 | I made a change to [#148528125] addressing the functionality. 10 | ``` 11 | 12 | The Pivotal Tracker API will also change story status by including "fixed", "completed", or "finished" within the square brackets, in addition to the story ID. You may use different cases or forms of these verbs, such as "Fix" or "FIXES", and they may appear before or after the story ID. For features, one of these keywords will put the story in the finished state. For chores, it will put the story in the accepted state. 13 | 14 | If code is automatically deployed, use the keyword "delivers" and feature stories will be put in the "delivered" state, rather than "completed." Examples: 15 | 16 | ```shell 17 | [Completed #148528125] adding requested feature. 18 | I finally [finished #148528125] this functionality. 19 | This commit [fixes #148528125] 20 | [Delivers #148528125] Small bug fix. 21 | ``` 22 | 23 | Commits that contain multiple Tracker stories will post activity to each story. Activity will be updated each time a commit is pushed to any dev or multidev branch; each message is labeled with the appropriate commit hash and Pantheon environment that triggered the post. 24 | 25 | ## Instructions ## 26 | 27 | - Copy your Tracker API token into a file called `secrets.json` and store it in the private files area of your site 28 | 29 | ```shell 30 | SITE= 31 | $ echo '{}' > secrets.json 32 | $ `terminus connection:info $SITE.dev --field=sftp_command` 33 | sftp> put ./files/private secrets.json 34 | sftp> bye 35 | terminus secrets:set $SITE.dev tracker_token 36 | pivotal-tracker $terminus secrets:list $SITE.dev //verify 37 | $ rm secrets.json 38 | 39 | ``` 40 | - Add the example `pivotal_integration.php` script to the `private` directory of your code repository (at the docroot, not in the aforementioned files/private directory). 41 | - Add a Quicksilver operation to your `pantheon.yml` to fire the script after a deploy. 42 | - Push code with a commit message containing a Pivotal story ID! 43 | 44 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. 45 | 46 | ### Example `pantheon.yml` ### 47 | 48 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 49 | 50 | ```yaml 51 | api_version: 1 52 | 53 | workflows: 54 | sync_code: 55 | after: 56 | - type: webphp 57 | description: Pivotal Integration 58 | script: private/pivotal_integration.php 59 | ``` 60 | -------------------------------------------------------------------------------- /pivotal-tracker/pivotal_integration.php: -------------------------------------------------------------------------------- 1 | /dev/null", $output, $status); 24 | if (!$status) { 25 | $last_commithash = $last_processed_commithash; 26 | } 27 | } 28 | // Update the last commit file with the latest commit 29 | file_put_contents($commit_file, $current_commithash, LOCK_EX); 30 | 31 | // Retrieve git log for commits after last processed, to current 32 | $commits = _get_commits($current_commithash, $last_commithash, $env); 33 | // Check each commit message for Pivotal ticket numbers 34 | foreach ($commits['pivotal'] as $ticket_id => $commit_ids) { 35 | foreach ($commit_ids as $commit_id) { 36 | send_commit($secrets, $commits['history'][$commit_id]); 37 | } 38 | } 39 | 40 | /** 41 | * Do git operations to find all commits between the specified commit hashes, 42 | * and return an associative array containing all applicable commits that 43 | * contain references to Pivotal Tracker issues. 44 | */ 45 | function _get_commits($current_commithash, $last_commithash, $env) { 46 | $commits = array( 47 | // Raw output of git log since the last processed 48 | 'history_raw' => null, 49 | // Formatted array of commits being sent to pivotal 50 | 'history' => array(), 51 | // An array keyed by pivotal ticket id, each holding an 52 | // array of commit ids. 53 | 'pivotal' => array() 54 | ); 55 | //builds command 56 | $cmd = 'git log'; // add -p to include diff 57 | if (!$last_commithash) { 58 | $cmd .= ' -n 1'; 59 | } 60 | else { 61 | $cmd .= ' ' . $last_commithash . '...' . $current_commithash; 62 | } 63 | $commits['history_raw'] = shell_exec($cmd); 64 | // Parse raw history into an array of commits 65 | $history = preg_split('/^commit /m', $commits['history_raw'], -1, PREG_SPLIT_NO_EMPTY); 66 | foreach ($history as $str) { 67 | $commit = array( 68 | 'full' => 'Commit: ' . $str 69 | ); 70 | // Only interested in the lines before the diff now 71 | $lines = explode("\n", $str); 72 | $commit['id'] = $lines[0]; 73 | $commit['message'] = trim(implode("\n", array_slice($lines, 4))); 74 | $commit['formatted'] = $commit['message'] . " " . $lines[1] . ' - ' . $lines[2]; 75 | // Look for matches in commit based on Pivotal Tracker issue ID format 76 | // Expected pattern: "[#12345678] commit message" or "[fixed|completed|finished #12345678 message]" 77 | preg_match('/\[[^#]*#\K\d{1,16}/', $commit['message'], $matches); 78 | if (count($matches) > 0) { 79 | // Build the $commits['pivotal'] array so there is 80 | // only 1 item per ticket id 81 | foreach ($matches as $ticket_id) { 82 | if (!isset($commits['pivotal'][$ticket_id])) { 83 | $commits['pivotal'][$ticket_id] = array(); 84 | } 85 | // ... and only 1 item per commit id 86 | $commits['pivotal'][$ticket_id][$commit['id']] = $commit['id']; 87 | } 88 | // Add the commit to the history array since there was a match. 89 | $commits['history'][$commit['id']] = $commit; 90 | } 91 | } 92 | return $commits; 93 | } 94 | 95 | /** 96 | * Send commits to Pivotal 97 | */ 98 | function send_commit($secrets, $commit) { 99 | print("\n==== Posting to Pivotal Tracker ====\n"); 100 | 101 | // Sends commit to pivotal. 102 | $ch = 'curl -X POST -H "X-TrackerToken: '. $secrets['tracker_token'] . '" -H "Content-Type: application/json" -d \'{"source_commit":{"commit_id":"' . substr($commit['id'], 0, 10) . '","message":"' . $commit['formatted'] . '"}\' "https://www.pivotaltracker.com/services/v5/source_commits?fields=%3Adefault%2Ccomments"'; 103 | 104 | $result = shell_exec($ch); 105 | print("RESULT: $result"); 106 | print("\n===== Post Complete! =====\n"); 107 | } 108 | 109 | /** 110 | * Get secrets from secrets file. 111 | * 112 | * @param array $requiredKeys List of keys in secrets file that must exist. 113 | */ 114 | function _get_secrets($requiredKeys, $defaults) 115 | { 116 | $secretsFile = $_SERVER['HOME'] . '/files/private/secrets.json'; 117 | if (!file_exists($secretsFile)) { 118 | die('No secrets file found. Aborting!'); 119 | } 120 | $secretsContents = file_get_contents($secretsFile); 121 | $secrets = json_decode($secretsContents, 1); 122 | if ($secrets == false) { 123 | die('Could not parse json in secrets file. Aborting!'); 124 | } 125 | $secrets += $defaults; 126 | $missing = array_diff($requiredKeys, array_keys($secrets)); 127 | if (!empty($missing)) { 128 | die('Missing required keys in json secrets file: ' . implode(',', $missing) . '. Aborting!'); 129 | } 130 | return $secrets; 131 | } 132 | -------------------------------------------------------------------------------- /quicksilver_pushback/README.md: -------------------------------------------------------------------------------- 1 | # Quicksilver Pushback # 2 | 3 | This Quicksilver project is used in conjunction with the various suite of [Terminus Build Tools](https://github.com/pantheon-systems/terminus-build-tools-plugin)-based example repositories to push any commits made on the Pantheon dashboard back to the original Git repository for the site. This allows developers (or other users) to work on the Pantheon dashboard in SFTP mode and commit their code, through Pantheon, back to the canonical upstream repository via a PR. This is especially useful in scenarios where you want to export configuration (Drupal, WP-CFM). 4 | 5 | This project is maintained in it's own repo located at https://github.com/pantheon-systems/quicksilver-pushback. Check that page for more information about the project, including installation instructions. Please note that it comes installed automatically if you use the Terminus Build Tools plugin, so you probably don't need to install it yourself unless you're following a non-standard workflow. 6 | -------------------------------------------------------------------------------- /slack_notification/README.md: -------------------------------------------------------------------------------- 1 | # Slack Integration 2 | 3 | Hook into platform workflows and post notifications to Slack. 4 | 5 | ## Instructions 6 | 7 | ### Set up the Slack App 8 | 1. [Navigate to api.slack.com/apps](https://api.slack.com/apps) while logged into your Slack workspace. 9 | 1. Click **Create New App**. 10 | 1. Choose **From scratch** in the **Create an app** modal. 11 | 1. Give your app a name (e.g. "Pantheon Deploybot") and select a workspace for your app. 12 | 1. Click **OAuth & Permissions** in the **Features** menu from the left sidebar of your app's configuration screen. 13 | 1. Scroll down to **Bot Token Scopes** under **Scopes** and click the **Add an OAuth Scope** button. 14 | 1. Choose `chat:write` from the dropdown. You may also add other relevant scopes if you plan on extending the Slack notification Quicksilver script's functionality. 15 | 1. Scroll up to **OAuth Tokens** and click the **Install to {your workspace}** button to install the app into your Slack instance. 16 | 1. Authorize ("Allow") the app for your workspace. 17 | 1. Copy the **Bot User OAuth Token** from the **OAuth Tokens** section. We will use [Pantheon Secrets](https://docs.pantheon.io/guides/secrets/overview) to store this token to a secret, bypassing the need for a local file with an API token stored in version control. 18 | 1. Invite your app to a channel (e.g. `/invite @Deploybot` in the channel you want to post notifications to). 19 | 1. You can customize any additional information about your bot, adding an avatar, etc. as you wish. 20 | 21 | At this point, you should be able to test the bot manually by sending a `curl` request to the Slack API: 22 | 23 | ```bash 24 | curl -X POST -H "Authorization: Bearer xoxb-YOUR-TOKEN" \ 25 | -H "Content-Type: application/json" \ 26 | -d '{ 27 | "channel": "#channel-name", 28 | "text": "Hello from Deploybot!" 29 | }' \ 30 | https://slack.com/api/chat.postMessage 31 | ``` 32 | 33 | ### Add the OAuth token to Pantheon Secrets 34 | 35 | 1. Install the [Terminus Secrets Manager Plugin](https://docs.pantheon.io/guides/secrets#installation). 36 | 1. Set the secret with the following command: `terminus secret:site:set --scope=web` 37 | - Replace `` with your site name (e.g. `my-site`). 38 | - Replace `` with the name of your secret that you will use in the code. In the example script, this is set to `slack_deploybot_token`. 39 | - Replace `` with the Bot User OAuth Token copied from the above steps. 40 | 1. Add the example `slack_notification.php` script to the `private` directory in the root of your site's codebase, that is under version control. 41 | 1. Update the `slack_notification.php` script to change the global variables used to create the Slack message. All of the variables are at the top of the file: 42 | - `$slack_channel` - Update this to change the channel that you wish to push notifications to. Defaults to `#firehose`. 43 | - `$type` - Update this to change the type of Slack API to use. `attachments` is the default and includes a yellow sidebar. `blocks` uses more modern API but lacks the sidebar. See [Slack's caveats for using "attachments"](https://api.slack.com/reference/surfaces/formatting#when-to-use-attachments). 44 | - `$secret_key` - The _key_ for the Pantheon Secret you created earlier. Defaults to `slack_deploybot_token`. 45 | 1. Make any other customizations of the script as you see fit. 46 | 1. Add Quicksilver operations to your `pantheon.yml` (see the [example](#example-pantheonyml) below). 47 | 1. Test a deployment and see the notification in the Slack channel associated with the webhook. 48 | 49 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback or use the [Workflow Logs](https://docs.pantheon.io/workflow-logs) to return any debugging output. 50 | 51 | **Note:** The example `slack_notification.php` script defaults to [message attachments](https://api.slack.com/reference/messaging/attachments) to keep the colored sidebar while using the updated API. This can be swapped out in favor of a [block-based](https://api.slack.com/reference/block-kit/blocks) approach entirely if that cosmetic detail is not important to you. To do this, simply change the `$type` from `'attachments'` to `'blocks'`. 52 | 53 | ### Example `pantheon.yml` 54 | 55 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use. Pick and choose the exact workflows that you would like to see notifications for. 56 | 57 | ```yaml 58 | api_version: 1 59 | 60 | workflows: 61 | deploy_product: 62 | after: 63 | - type: webphp 64 | description: Post to Slack after site creation 65 | script: private/scripts/slack_notification.php 66 | create_cloud_development_environment: 67 | after: 68 | - type: webphp 69 | description: Post to Slack after Multidev creation 70 | script: private/scripts/slack_notification.php 71 | deploy: 72 | after: 73 | - type: webphp 74 | description: Post to Slack after deploy 75 | script: private/scripts/slack_notification.php 76 | sync_code: 77 | after: 78 | - type: webphp 79 | description: Post to Slack after code commit 80 | script: private/scripts/slack_notification.php 81 | clear_cache: 82 | after: 83 | - type: webphp 84 | description: Someone is clearing the cache again 85 | script: private/scripts/slack_notification.php 86 | ``` 87 | -------------------------------------------------------------------------------- /slack_notification/slack_notification.php: -------------------------------------------------------------------------------- 1 | $block_type, 34 | 'text' => [ 35 | 'type' => $type, 36 | 'text' => $text, 37 | ], 38 | ]; 39 | } 40 | 41 | /** 42 | * A multi-column block of content (very likely 2 cols) 43 | * 44 | * @param array $fields The fields to send to the multi-column block. 45 | * @return array 46 | */ 47 | function _create_multi_block( array $fields ) { 48 | return [ 49 | 'type' => 'section', 50 | 'fields' => array_map( function( $field ) { 51 | return [ 52 | 'type' => 'mrkdwn', 53 | 'text' => $field, 54 | ]; 55 | }, $fields ) 56 | ]; 57 | } 58 | 59 | 60 | /** 61 | * Creates a context block for a Slack message. 62 | * 63 | * @param array $elements An array of text elements to be included in the context block. 64 | * @return array The context block formatted for a Slack message. 65 | */ 66 | function _create_context_block( array $elements ) { 67 | return [ 68 | 'type' => 'context', 69 | 'elements' => array_map( function( $element ) { 70 | return [ 71 | 'type' => 'mrkdwn', 72 | 'text' => $element, 73 | ]; 74 | }, $elements ), 75 | ]; 76 | } 77 | 78 | /** 79 | * A divider block 80 | * 81 | * @return array 82 | */ 83 | function _create_divider_block() { 84 | return ['type' => 'divider']; 85 | } 86 | 87 | /** 88 | * Some festive emoji for the Slack message based on the workflow we're running. 89 | * 90 | * @return array 91 | */ 92 | function _get_emoji() { 93 | // Edit these if you want to change or add to the emoji used in Slack messages. 94 | return [ 95 | 'deploy' => ':rocket:', 96 | 'sync_code' => ':computer:', 97 | 'sync_code_external_vcs' => ':computer:', 98 | 'clear_cache' => ':broom:', 99 | 'clone_database' => ':man-with-bunny-ears-partying:', 100 | 'deploy_product' => ':magic_wand:', 101 | 'create_cloud_development_environment' => ':lightning_cloud:', 102 | ]; 103 | } 104 | 105 | /** 106 | * Get the type of the current workflow from the $_POST superglobal. 107 | * 108 | * @return string 109 | */ 110 | function _get_workflow_type() { 111 | return $_POST['wf_type']; 112 | } 113 | 114 | /** 115 | * Extract a human-readable workflow name from the workflow type. 116 | * 117 | * @return string 118 | */ 119 | function _get_workflow_name() { 120 | return ucfirst(str_replace('_', ' ', _get_workflow_type())); 121 | } 122 | 123 | // Uncomment the following line to see the workflow type. 124 | // printf("Workflow type: %s\n", _get_workflow_type()); 125 | 126 | /** 127 | * Get Pantheon environment variables from the $_ENV superglobal. 128 | * 129 | * @return object 130 | */ 131 | function _get_pantheon_environment() { 132 | $pantheon_env = new stdClass; 133 | $pantheon_env->site_name = $_ENV['PANTHEON_SITE_NAME']; 134 | $pantheon_env->environment = $_ENV['PANTHEON_ENVIRONMENT']; 135 | 136 | return $pantheon_env; 137 | } 138 | 139 | /** 140 | * Create base blocks for all workflows. 141 | * 142 | * @return array 143 | */ 144 | function _create_base_blocks() { 145 | $icons = _get_emoji(); 146 | $workflow_type = _get_workflow_type(); 147 | $workflow_name = _get_workflow_name(); 148 | $environment = _get_pantheon_environment()->environment; 149 | $site_name = _get_pantheon_environment()->site_name; 150 | 151 | $blocks = [ 152 | _create_text_block( "{$icons[$workflow_type]} {$workflow_name}", 'plain_text', 'header' ), 153 | _create_multi_block([ 154 | "*Site:* ", 155 | "*Environment:* ", 156 | "*Initiated by:* {$_POST['user_email']}", 157 | ]), 158 | ]; 159 | 160 | return $blocks; 161 | } 162 | 163 | /** 164 | * Add custom blocks based on the workflow type. 165 | * 166 | * Note that slack_notification.php must appear in your pantheon.yml for each workflow type you wish to send notifications on. 167 | * 168 | * @return array 169 | */ 170 | function _get_blocks_for_workflow() { 171 | $workflow_type = _get_workflow_type(); 172 | $blocks = _create_base_blocks(); 173 | $environment = _get_pantheon_environment()->environment; 174 | $site_name = _get_pantheon_environment()->site_name; 175 | 176 | switch ($workflow_type) { 177 | case 'deploy': 178 | $deploy_message = $_POST['deploy_message']; 179 | $blocks[] = _create_text_block("*Deploy Note:*\n{$deploy_message}"); 180 | break; 181 | 182 | case 'sync_code': 183 | case 'sync_code_external_vcs': 184 | // Get the time, committer, and message for the most recent commit 185 | $committer = trim(`git log -1 --pretty=%cn`); 186 | $hash = trim(`git log -1 --pretty=%h`); 187 | $message = trim(`git log -1 --pretty=%B`); 188 | $blocks[] = _create_multi_block([ 189 | "*Commit:* {$hash}", 190 | "*Committed by:* {$committer}", 191 | ]); 192 | $blocks[] = _create_text_block("*Commit Message:*\n{$message}"); 193 | break; 194 | 195 | case 'clear_cache': 196 | $blocks[] = _create_text_block("*Action:*\nCaches cleared on ."); 197 | break; 198 | 199 | case 'clone_database': 200 | $blocks[] = _create_multi_block([ 201 | "*Cloned from:* {$_POST['from_environment']}", 202 | "*Cloned to:* {$environment}", 203 | ]); 204 | break; 205 | 206 | default: 207 | $description = $_POST['qs_description'] ?? 'No additional details provided.'; 208 | $blocks[] = _create_text_block("*Description:*\n{$description}"); 209 | break; 210 | } 211 | 212 | // Add a divider block at the end of the message 213 | $blocks[] = _create_divider_block(); 214 | 215 | return $blocks; 216 | } 217 | 218 | // Uncomment to debug the blocks. 219 | // echo "Blocks:\n"; 220 | // print_r( _get_blocks_for_workflow() ); 221 | 222 | /** 223 | * Prepare Slack POST content as an attachment with yellow sidebar. 224 | * 225 | * @return array 226 | */ 227 | function _get_attachments() { 228 | global $pantheon_yellow; 229 | return [ 230 | [ 231 | 'color' => $pantheon_yellow, 232 | 'blocks' => _get_blocks_for_workflow(), 233 | ] 234 | ]; 235 | } 236 | 237 | // Uncomment the following line to debug the attachments array. 238 | // echo "Attachments:\n"; print_r( $attachments ); echo "\n"; 239 | 240 | /** 241 | * Send a notification to Slack 242 | */ 243 | function _post_to_slack() { 244 | global $slack_channel, $type, $secret_key; 245 | 246 | $attachments = _get_attachments(); 247 | $blocks = _get_blocks_for_workflow(); 248 | $slack_token = pantheon_get_secret($secret_key); 249 | 250 | $post['channel'] = $slack_channel; 251 | 252 | // Check the type and adjust the payload accordingly. 253 | if ( $type === 'attachments' ) { 254 | $post['attachments'] = $attachments; 255 | } elseif ( $type === 'blocks' ) { 256 | $post['blocks'] = $blocks; 257 | } else { 258 | throw new InvalidArgumentException("Unsupported type: $type"); 259 | } 260 | 261 | // Uncomment to debug the payload. 262 | // echo "Payload: " . json_encode($post, JSON_PRETTY_PRINT) . "\n"; 263 | $payload = json_encode($post); 264 | 265 | $ch = curl_init(); 266 | curl_setopt($ch, CURLOPT_URL, 'https://slack.com/api/chat.postMessage'); 267 | curl_setopt($ch, CURLOPT_HTTPHEADER, [ 268 | 'Authorization: Bearer ' . $slack_token, 269 | 'Content-Type: application/json; charset=utf-8', 270 | ]); 271 | curl_setopt($ch, CURLOPT_POST, 1); 272 | curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); 273 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 274 | curl_setopt($ch, CURLOPT_TIMEOUT, 5); 275 | 276 | print("\n==== Posting to Slack ====\n"); 277 | $result = curl_exec($ch); 278 | $response = json_decode($result, true); 279 | 280 | if (!$response['ok']) { 281 | print("Error: " . $response['error'] . "\n"); 282 | error_log("Slack API error: " . $response['error']); 283 | } else { 284 | print("Message sent successfully!\n"); 285 | } 286 | 287 | curl_close($ch); 288 | } 289 | 290 | // Send the Slack notification 291 | _post_to_slack(); -------------------------------------------------------------------------------- /teams_notification/README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Teams Integration # 2 | 3 | This script shows how easy it is to integrate Microsoft Teams notifications from your Pantheon project using Quicksilver. 4 | 5 | ## Instructions ## 6 | 7 | Setting up this example is quite easy: 8 | 9 | - Within your Microsoft Teams, go to the channel in Teams where you want to integrate the notification and click on the ••• near the name of the channel. 10 | - Click on "Connectors" and search for the Connector "Incoming Webhook" then click on "Configure" 11 | - Set "Pantheon" as the name of this Connector and upload the Pantheon logo image to customize the image this Connector 12 | - Copy the webhook URL into a file called `secrets.json` and store it in the private files area of your site 13 | 14 | ```shell 15 | $> echo '{"teams_url": "https://outlook.office.com/webhook/HOOK_URL"}' > secrets.json 16 | # Note, you'll need to copy the secrets into each environment where you want to trigger Microsoft Teams notifications. 17 | $> `terminus site connection-info --env=dev --site=your-site --field=sftp_command` 18 | Connected to appserver.dev.d1ef01f8-364c-4b91-a8e4-f2a46f14237e.drush.in. 19 | sftp> cd files 20 | sftp> mkdir private 21 | sftp> cd private 22 | sftp> put secrets.json 23 | ``` 24 | - Add the example `teams_notifications` directory to the `private/scripts` directory of your code repository. 25 | - Add the Quicksilver operations to your `pantheon.yml` to fire the scripts when code is synced and deployed. 26 | 27 | Optionally, you may want to customize your notifications further. In that case, you can update the JSON files stored in the /samples folder. It based on MessageCard that is defined [here](https://docs.microsoft.com/en-us/outlook/actionable-messages/message-card-reference) by Microsoft official documentation. 28 | Note that AdapativeCard format doesn't work with webhook Connector. 29 | 30 | ### Example `pantheon.yml` ### 31 | 32 | Here's an example of what your `pantheon.yml` would look like if these were the only Quicksilver operations you wanted to use: 33 | 34 | ```yaml 35 | api_version: 1 36 | 37 | workflows: 38 | deploy: 39 | after: 40 | - type: webphp 41 | description: Microsoft Teams Notification - Deploy 42 | script: private/scripts/teams_notification/teams_notification.php 43 | sync_code: 44 | after: 45 | - type: webphp 46 | description: Microsoft Teams Notification - Sync 47 | script: private/scripts/teams_notification/teams_notification.php 48 | ``` 49 | -------------------------------------------------------------------------------- /teams_notification/samples/clear_cache_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "summary": "Card for Deploy message", 3 | "themeColor": "0072C6", 4 | "title": "Cleared cache on \"{{PROJECT_NAME}}\" by", 5 | "sections": [ 6 | { 7 | "activityTitle": "{{USERNAME}}", 8 | "activitySubtitle": "{{USERMAIL}}", 9 | "activityImage": "https://secure.gravatar.com/avatar/{{USERMAIL_HASH}}", 10 | "facts": [ 11 | { 12 | "name": "Environment:", 13 | "value": "{{ENV}}" 14 | } 15 | ], 16 | "text": "Cache cleared" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /teams_notification/samples/deploy_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "summary": "Card for Deploy message", 3 | "themeColor": "0072C6", 4 | "title": "New deployment on \"{{PROJECT_NAME}}\" by", 5 | "sections": [ 6 | { 7 | "activityTitle": "{{USERNAME}}", 8 | "activitySubtitle": "{{USERMAIL}}", 9 | "activityImage": "https://secure.gravatar.com/avatar/{{USERMAIL_HASH}}", 10 | "facts": [ 11 | { 12 | "name": "Environment:", 13 | "value": "{{ENV}}" 14 | }, 15 | { 16 | "name": "Tag", 17 | "value": "{{DEPLOY_TAG}}" 18 | } 19 | ], 20 | "text": "Deploy note: _{{DEPLOY_NOTE}}_" 21 | } 22 | ], 23 | "potentialAction": [ 24 | { 25 | "@type": "OpenUri", 26 | "name": "View deploy log", 27 | "targets": [ 28 | { "os": "default", "uri": "{{DEPLOY_LOG_URL}}" } 29 | ] 30 | }, 31 | { 32 | "@type": "OpenUri", 33 | "name": "View website", 34 | "targets": [ 35 | { "os": "default", "uri": "{{ENV_URL}}" } 36 | ] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /teams_notification/samples/sync_code_msg.json: -------------------------------------------------------------------------------- 1 | { 2 | "summary": "Card for Deploy message", 3 | "themeColor": "0072C6", 4 | "title": "New commit on \"{{PROJECT_NAME}}\" by", 5 | "sections": [ 6 | { 7 | "activityTitle": "{{USERNAME}}", 8 | "activitySubtitle": "{{USERMAIL}}", 9 | "activityImage": "https://secure.gravatar.com/avatar/{{USERMAIL_HASH}}", 10 | "facts": [ 11 | { 12 | "name": "Environment:", 13 | "value": "{{ENV}}" 14 | } 15 | ], 16 | "text": "{{MESSAGE}}" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /teams_notification/teams_notification.php: -------------------------------------------------------------------------------- 1 | $_POST['user_email'], 12 | 'USERMAIL_HASH' => md5(strtolower(trim($_POST['user_email']))), 13 | 'USERNAME' => $_POST['user_fullname'], 14 | 'ENV' => $_ENV['PANTHEON_ENVIRONMENT'], 15 | 'PROJECT_NAME' => $_ENV['PANTHEON_SITE_NAME'] 16 | ]; 17 | 18 | switch($_POST['wf_type']) { 19 | case 'deploy': 20 | 21 | // Find out what tag we are on and last commit date 22 | $deploy_tag= `git describe --tags`; 23 | 24 | // [TODO] needs to be more accurate with exact date of the creation of tag and not last commit date 25 | $deploy_date = `git log -1 --format=%ai `; 26 | 27 | // Set additional parameters for deploy case 28 | $params += [ 29 | 'DEPLOY_NOTE' => $_POST['deploy_message'], 30 | 'DEPLOY_TAG' => $deploy_tag, 31 | 'DEPLOY_LOG_URL' => 'https://dashboard.pantheon.io/sites/' . $_ENV['PANTHEON_SITE'] . '#'. strtolower($_ENV['PANTHEON_ENVIRONMENT']) .'/deploys', 32 | 'ENV_URL' => 'https://' . $_ENV['PANTHEON_ENVIRONMENT'] . '-' . $_ENV['PANTHEON_SITE_NAME'] . '.pantheonsite.io' 33 | ]; 34 | 35 | // Get the "deploy" message template 36 | $message = file_get_contents("samples/deploy_msg.json"); 37 | 38 | break; 39 | 40 | case 'sync_code': 41 | $committer = `git log -1 --pretty=%cn`; 42 | $email = `git log -1 --pretty=%ce`; 43 | $message = `git log -1 --pretty=%B`; 44 | $hash = `git log -1 --pretty=%h`; 45 | 46 | $text = 'Most recent commit: '. rtrim($hash) . ' by ' . rtrim($committer) . ' (' . $email . '): ' . $message; 47 | 48 | $params += [ 49 | 'MESSAGE' => $text 50 | ]; 51 | 52 | // Get the "sync code" message template 53 | $message = file_get_contents("samples/sync_code_msg.json"); 54 | 55 | break; 56 | 57 | // [TODO] not working for now 58 | case 'clear_cache': 59 | // Get the "clear cache" message template 60 | $message = file_get_contents("samples/clear_cache_msg.json"); 61 | 62 | break; 63 | 64 | default: 65 | //$text = "Workflow $workflow_description
" . $_POST['qs_description']; 66 | break; 67 | 68 | } 69 | 70 | $message = preg_replace_callback('/{{((?:[^}]|}[^}])+)}}/', function($match) use ($params) { return ($params[$match[1]]); }, $message); 71 | 72 | _teams_notification($secrets['teams_url'],$message); 73 | 74 | /** 75 | * Get secrets from secrets file. 76 | * 77 | * @param array $requiredKeys List of keys in secrets file that must exist. 78 | */ 79 | function _get_secrets($requiredKeys, $defaults) 80 | { 81 | $secretsFile = $_SERVER['HOME'] . '/files/private/secrets.json'; 82 | if (!file_exists($secretsFile)) { 83 | die('No secrets file found. Aborting!'); 84 | } 85 | $secretsContents = file_get_contents($secretsFile); 86 | $secrets = json_decode($secretsContents, 1); 87 | if ($secrets == false) { 88 | die('Could not parse json in secrets file. Aborting!'); 89 | } 90 | $secrets += $defaults; 91 | $missing = array_diff($requiredKeys, array_keys($secrets)); 92 | if (!empty($missing)) { 93 | die('Missing required keys in json secrets file: ' . implode(',', $missing) . '. Aborting!'); 94 | } 95 | return $secrets; 96 | } 97 | 98 | 99 | /** 100 | * Send notifications to Microsoft Teams 101 | */ 102 | function _teams_notification($teams_url,$message){ 103 | 104 | $ch = curl_init(); 105 | curl_setopt($ch, CURLOPT_URL, $teams_url); 106 | curl_setopt($ch, CURLOPT_POST, 1); 107 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 108 | curl_setopt($ch, CURLOPT_TIMEOUT, 5); 109 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json')); 110 | curl_setopt($ch, CURLOPT_POSTFIELDS, $message); 111 | 112 | // Watch for messages with `terminus workflows watch --site=SITENAME` 113 | print("\n==== Posting to Teams ====\n"); 114 | $result = curl_exec($ch); 115 | print("RESULT: $result"); 116 | print("MESSAGE SENT: $message"); 117 | print("\n===== Post Complete! =====\n"); 118 | curl_close($ch); 119 | } 120 | -------------------------------------------------------------------------------- /trello_integration/README.md: -------------------------------------------------------------------------------- 1 | # Trello Integration # 2 | 3 | This example parses commit messages for Trello card IDs and adds the commit message as a comment in the related Trello card. 4 | 5 | Example comments: 6 | 7 | [s3yxNR5v]: Adjust layout spacing 8 | 9 | Commits that contain multiple Trello cards will post comments to each issue mentioned. A comment will be added each time a commit is pushed to any dev or multidev branch; each Trello comment is labeled with the appropriate commit hash and Pantheon environment that triggered the post. 10 | 11 | ## Instructions ## 12 | 13 | - Go to https://trello.com/app-key and copy your app key. Also click the link to generate a token for yourself, approve access and copy the token. 14 | - Copy your Trello credentials (key + token) into a file called `secrets.json` and store it in the private files area of your site 15 | 16 | ```shell 17 | $> echo '{"trello_key" : "Your App Key" , "trello_token" : "Your generated token" }' > secrets.json 18 | # Note, you'll need to copy the secrets into each environment where you want to save commit messages to Trello 19 | $> `terminus site connection-info --env=dev --site=your-site --field=sftp_command` 20 | Connected to appserver.dev.d1ef01f8-364c-4b91-a8e4-f2a46f14237e.drush.in. 21 | sftp> cd files 22 | sftp> mkdir private 23 | sftp> cd private 24 | sftp> put secrets.json 25 | 26 | ``` 27 | - Add the example `trello_integration.php` script to the `private` directory of your code repository. 28 | - Add a Quicksilver operation to your `pantheon.yml` to fire the script after a deploy. 29 | - Push code with a commit message containing a Trello card ID! 30 | 31 | Optionally, you may want to use the `terminus workflow:watch` command to get immediate debugging feedback. 32 | 33 | ### Example `pantheon.yml` ### 34 | 35 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 36 | 37 | ```yaml 38 | api_version: 1 39 | 40 | workflows: 41 | sync_code: 42 | after: 43 | - type: webphp 44 | description: Trello Integration 45 | script: private/scripts/trello_integration.php 46 | ``` 47 | -------------------------------------------------------------------------------- /trello_integration/trello_integration.php: -------------------------------------------------------------------------------- 1 | /dev/null", $output, $status); 21 | if (!$status) { 22 | $last_commithash = $last_processed_commithash; 23 | } 24 | } 25 | // Update the last commit file with the latest commit 26 | file_put_contents($commit_file, $current_commithash, LOCK_EX); 27 | 28 | // Retrieve git log for commits after last processed, to current 29 | $commits = _get_commits($current_commithash, $last_commithash, $env); 30 | 31 | // Check each commit message for Trello card IDs 32 | foreach ($commits['trello'] as $card_id => $commit_ids) { 33 | foreach ($commit_ids as $commit_id) { 34 | send_commit($secrets, $card_id, $commits['history'][$commit_id]); 35 | } 36 | } 37 | 38 | /** 39 | * Do git operations to find all commits between the specified commit hashes, 40 | * and return an associative array containing all applicable commits that 41 | * contain references to Trello cards. 42 | */ 43 | function _get_commits($current_commithash, $last_commithash, $env) { 44 | $commits = array( 45 | // Raw output of git log since the last processed 46 | 'history_raw' => null, 47 | // Formatted array of commits being sent to Trello 48 | 'history' => array(), 49 | // An array keyed by Trello card id, each holding an 50 | // array of commit ids. 51 | 'trello' => array() 52 | ); 53 | 54 | $cmd = 'git log'; // add -p to include diff 55 | if (!$last_commithash) { 56 | $cmd .= ' -n 1'; 57 | } 58 | else { 59 | $cmd .= ' ' . $last_commithash . '...' . $current_commithash; 60 | } 61 | $commits['history_raw'] = shell_exec($cmd); 62 | // Parse raw history into an array of commits 63 | $history = preg_split('/^commit /m', $commits['history_raw'], -1, PREG_SPLIT_NO_EMPTY); 64 | foreach ($history as $str) { 65 | $commit = array( 66 | 'full' => 'Commit: ' . $str 67 | ); 68 | // Only interested in the lines before the diff now 69 | $lines = explode("\n", $str); 70 | $commit['id'] = $lines[0]; 71 | $commit['message'] = trim(implode("\n", array_slice($lines, 4))); 72 | $commit['formatted'] = 'Commit: ' . substr($commit['id'], 0, 10) . ' [' . $env . '] 73 | ' . $commit['message'] . ' 74 | ~' . $lines[1] . ' - ' . $lines[2]; 75 | // Look for matches on a Trello card ID format 76 | // = [8 characters] 77 | preg_match('/\[[a-zA-Z0-9]{8}\]/', $commit['message'], $matches); 78 | if (count($matches) > 0) { 79 | // Build the $commits['trello'] array so there is 80 | // only 1 item per ticket id 81 | foreach ($matches as $card_id_enc) { 82 | $card_id = substr($card_id_enc, 1, -1); 83 | if (!isset($commits['trello'][$card_id])) { 84 | $commits['trello'][$card_id] = array(); 85 | } 86 | // ... and only 1 item per commit id 87 | $commits['trello'][$card_id][$commit['id']] = $commit['id']; 88 | } 89 | // Add the commit to the history array since there was a match. 90 | $commits['history'][$commit['id']] = $commit; 91 | } 92 | } 93 | return $commits; 94 | } 95 | 96 | /** 97 | * Send commits to Trello 98 | */ 99 | function send_commit($secrets, $card_id, $commit) { 100 | $payload = array( 101 | 'text' => $commit['formatted'], 102 | 'key' => $secrets['trello_key'], 103 | 'token' => $secrets['trello_token'] 104 | ); 105 | $ch = curl_init(); 106 | curl_setopt($ch, CURLOPT_URL, 'https://api.trello.com/1/cards/' . $card_id . '/actions/comments'); 107 | curl_setopt($ch, CURLOPT_POST, 1); 108 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 109 | curl_setopt($ch, CURLOPT_TIMEOUT, 5); 110 | curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); 111 | print("\n==== Posting to Trello ====\n"); 112 | $result = curl_exec($ch); 113 | print("RESULT: $result"); 114 | print("\n===== Post Complete! =====\n"); 115 | curl_close($ch); 116 | } 117 | 118 | /** 119 | * Get secrets from secrets file. 120 | * 121 | * @param array $requiredKeys List of keys in secrets file that must exist. 122 | */ 123 | function _get_secrets($requiredKeys, $defaults) 124 | { 125 | $secretsFile = $_SERVER['HOME'] . '/files/private/secrets.json'; 126 | if (!file_exists($secretsFile)) { 127 | die('No secrets file ['.$secretsFile.'] found. Aborting!'); 128 | } 129 | $secretsContents = file_get_contents($secretsFile); 130 | $secrets = json_decode($secretsContents, 1); 131 | if ($secrets == false) { 132 | die('Could not parse json in secrets file. Aborting!'); 133 | } 134 | $secrets += $defaults; 135 | $missing = array_diff($requiredKeys, array_keys($secrets)); 136 | if (!empty($missing)) { 137 | die('Missing required keys in json secrets file: ' . implode(',', $missing) . '. Aborting!'); 138 | } 139 | return $secrets; 140 | } 141 | -------------------------------------------------------------------------------- /url_checker/README.md: -------------------------------------------------------------------------------- 1 | # URL Checker # 2 | 3 | This example demonstrates how check specific URLs after a live deployment. Failures are notified by email. 4 | 5 | Note: This example could also be used to warm up cache after a live deployment. 6 | 7 | ## Instructions ## 8 | 9 | - Copy the example `url_checker` directory to the `private/scripts` directory of your code repository. 10 | - Customize the config.json file as needed; specify URLs to test, and email address to send notifications to. 11 | - Add a Quicksilver operation to your `pantheon.yml` to fire the script after a deploy. Be sure to target the file for your platform. 12 | - Test a deploy out! 13 | 14 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. 15 | 16 | Here is an example of what you might see when using `terminus workflows watch`: 17 | 18 | ``` 19 | URL Checks 20 | -------- 21 | 200 - https://example.com/ 22 | 200 - https://example.com/user 23 | 404 - https://example.com/bad-path 24 | -------- 25 | 1 failed 26 | ``` 27 | 28 | ### Example `pantheon.yml` ### 29 | 30 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 31 | 32 | ```yaml 33 | api_version: 1 34 | 35 | workflows: 36 | deploy: 37 | after: 38 | - type: webphp 39 | description: URL Checker 40 | script: private/scripts/url_checker/url_checker.php 41 | ``` 42 | -------------------------------------------------------------------------------- /url_checker/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "ops@example.com", 3 | "base_url": "https://example.com/", 4 | "check_paths": [ 5 | "", 6 | "user", 7 | "bad-path" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /url_checker/url_checker.php: -------------------------------------------------------------------------------- 1 | $config['base_url'] . $path, 33 | 'status' => $status 34 | ); 35 | if ($status != 200) { 36 | $failed++; 37 | } 38 | } 39 | 40 | $output = url_checker_build_output($results, $failed); 41 | print $output; 42 | 43 | // If there were any URLs that could not be accessed, and if an email 44 | // address is configured in config.json, then send a notification email. 45 | if (($failed > 0) && (isset($config['email']))) { 46 | $subject = 'Failed status check (' . $failed . ')'; 47 | $message = "Below is a list of each tested url and its status:\n\n"; 48 | $message .= $output; 49 | $acceptedForDelivery = mail($config['email'], $subject, $message); 50 | if ($acceptedForDelivery) { 51 | print "Sent email to {$config['email']}.\n"; 52 | } 53 | else { 54 | print "Notification email to {$config['email']} could not be queued for delivery.\n"; 55 | } 56 | } 57 | 58 | /** 59 | * Returns decoded config defined in config.json 60 | */ 61 | function url_checker_get_config() { 62 | $config_file = __DIR__ . '/config.json'; 63 | if (!file_exists($config_file)) { 64 | die('Config file not found.'); 65 | } 66 | $config_file_contents = file_get_contents($config_file); 67 | if (empty($config_file_contents)) { 68 | die('Config file could not be read.'); 69 | } 70 | $config = json_decode($config_file_contents); 71 | if (!$config) { 72 | die('Config file did not contain valid json.'); 73 | } 74 | // Convert json object to an array. 75 | return (array) $config; 76 | } 77 | 78 | /** 79 | * Constructs workflow output 80 | */ 81 | function url_checker_build_output($results, $failed) { 82 | $output = "\nURL Checks\n--------\n"; 83 | foreach ($results as $item) { 84 | $output .= ' ' . $item['status'] . ' - ' . $item['url'] . "\n"; 85 | } 86 | $output .= "--------\n" . $failed . " failed\n\n"; 87 | return $output; 88 | } 89 | 90 | /** 91 | * Try to access the specified URL, and return the http status code. 92 | */ 93 | function url_checker_test_url($url) { 94 | $ch = curl_init($url); 95 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 96 | curl_exec($ch); 97 | $info = curl_getinfo($ch); 98 | curl_close($ch); 99 | return $info['http_code']; 100 | } 101 | -------------------------------------------------------------------------------- /webhook/README.md: -------------------------------------------------------------------------------- 1 | # Webhook # 2 | 3 | This example demonstrates how to POST workflow data to an external url, a generic webhook implementation. 4 | 5 | ## Instructions ## 6 | 7 | Setting up this example is easy: 8 | 9 | - Copy the `webhook` example directory to the `private/scripts` directory of your code repository. 10 | - Update the `$url` value in `private/scripts/webhook/webhook.php` to the destination you would like the workflow data to be posted. 11 | - Add the Quicksilver operations in the example below to your `pantheon.yml`. 12 | - Clear cache, sync code, clone a db, or deploy and take a look at your webhook handler for events! 13 | 14 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. 15 | 16 | ### Example `pantheon.yml` ### 17 | 18 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 19 | 20 | ```yaml 21 | api_version: 1 22 | 23 | workflows: 24 | clear_cache: 25 | after: 26 | - type: webphp 27 | description: Webhook 28 | script: private/scripts/webhook.php 29 | clone_database: 30 | after: 31 | - type: webphp 32 | description: Webhook 33 | script: private/scripts/webhook.php 34 | deploy: 35 | after: 36 | - type: webphp 37 | description: Webhook 38 | script: private/scripts/webhook.php 39 | sync_code: 40 | after: 41 | - type: webphp 42 | description: Webhook 43 | script: private/scripts/webhook.php 44 | ``` 45 | 46 | Depending on your use case, you may use the `before` as well as (or instead of) the `after` timing variant. 47 | 48 | ### Example POST data sent to the webhook ### 49 | 50 | Below is an example of the data sent as a POST request to the `url` defined in your script. 51 | 52 | ``` 53 | Array 54 | ( 55 | [payload] => Array 56 | ( 57 | [wf_type] => sync_code 58 | [user_id] => af9d4c14-9fd2-4053-aee2-7daf88fb73b5 59 | [user_firstname] => Finn 60 | [user_lastname] => Mertens 61 | [user_fullname] => Finn Mertens 62 | [site_id] => 0f97107a-e292-431b-aa3e-46f2301f5f82 63 | [user_role] => owner 64 | [trace_id] => 1089ead4-c3e2-11e3-a7f5-bc764e10b0cb 65 | [site_name] => adventure-time 66 | [environment] => dev 67 | [wf_description] => Sync code on "dev" 68 | [user_email] => fmartens@example.com 69 | ) 70 | 71 | ) 72 | ``` -------------------------------------------------------------------------------- /webhook/webhook.php: -------------------------------------------------------------------------------- 1 | &1' ); 5 | 6 | // Automagically import config into WP-CFM site upon code deployment 7 | $path = $_SERVER['DOCUMENT_ROOT'] . '/private/config'; 8 | $files = scandir( $path ); 9 | $files = array_diff( scandir( $path ), array( '.', '..' ) ); 10 | 11 | // Import all config .json files in private/config 12 | foreach( $files as $file ){ 13 | 14 | $file_parts = pathinfo($file); 15 | 16 | if( $file_parts['extension'] != 'json' ){ 17 | continue; 18 | } 19 | 20 | exec( 'wp config pull ' . $file_parts['filename'] . ' 2>&1', $output ); 21 | 22 | if ( count( $output ) > 0 ) { 23 | $output = preg_replace( '/\s+/', ' ', array_slice( $output, 1, - 1 ) ); 24 | $output = str_replace( ' update', ' [update]', $output ); 25 | $output = str_replace( ' create', ' [create]', $output ); 26 | $output = str_replace( ' delete', ' [delete]', $output ); 27 | $output = implode( $output, "\n" ); 28 | $output = rtrim( $output ); 29 | } 30 | } 31 | 32 | // Flush the cache 33 | exec( 'wp cache flush' ); 34 | 35 | print( "\n==== WP-CFM Config Import Complete ====\n" ); 36 | -------------------------------------------------------------------------------- /wp_search_replace/README.md: -------------------------------------------------------------------------------- 1 | # Search and Replace URLs on WordPress Sites # 2 | 3 | This example will show you how you can automatically find and replace URLs in the database on a WordPress website. This practice can help smooth out workflow gotchas with sites that have multiple domains in an environment. 4 | 5 | ## Instructions ## 6 | 7 | Setting up this example is easy: 8 | 9 | 1. Add the wp_search_replace.php to the `private` directory of your code repository. 10 | 2. Add a Quicksilver operation to your `pantheon.yml` to fire the script a deploy. 11 | 3. Test a deploy out! 12 | 13 | Optionally, you may want to use the `terminus workflows watch` command to get immediate debugging feedback. 14 | 15 | ### Example `pantheon.yml` ### 16 | 17 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use: 18 | 19 | ```yaml 20 | 21 | api_version: 1 22 | 23 | workflows: 24 | clone_database: 25 | after: 26 | - type: webphp 27 | description: Search and replace url in database 28 | script: private/scripts/search-replace-example.php 29 | ``` 30 | -------------------------------------------------------------------------------- /wp_search_replace/wp_search_replace.php: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /wp_solr_index/README.md: -------------------------------------------------------------------------------- 1 | # Solr Power Indexing on Multidev Creation for WordPress # 2 | 3 | When you create a new multidev environment the code, files and database are cloned but not the Solr instance. This script will re-index Solr using WP-CLI and the [Solr Power WordPress](https://github.com/pantheon-systems/solr-power) plugin. 4 | 5 | ## Instructions ## 6 | 7 | 1. Install and activate the Solr Power WordPress plugin in the dev environment. 8 | 2. Add the example `solr_power_index.php` script to the `private/scripts` directory in the root of your site's codebase, that is under version control. 9 | 3. Add the Quicksilver operations to your `pantheon.yml`. 10 | 4. Deploy `pantheon.yml` to the dev environment. 11 | 5. Create a new multidev instance 12 | * Optionally run `terminus workflows watch` locally to get feedback from the Quicksilver script. 13 | 6. After indexing completes, test out search or other items using Solr. 14 | 15 | ### Example `pantheon.yml` ### 16 | 17 | Here's an example of what your `pantheon.yml` would look like if this were the only Quicksilver operation you wanted to use. 18 | 19 | ```yaml 20 | api_version: 1 21 | 22 | workflows: 23 | create_cloud_development_environment: 24 | after: 25 | - type: webphp 26 | description: Index Solr Power items after multidev creation 27 | script: private/scripts/wp_solr_power_index.php 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /wp_solr_index/wp_solr_power_index.php: -------------------------------------------------------------------------------- 1 |