Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 23 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,35 +383,37 @@ assembles it from the Git log.

## Automatic Deployment to Pantheon

In order to deploy upon every merge automatically using GitHub Actions, you shall:
### Prerequisites

1. Initiate QA (`qa` branch) multidev environment for the given project.
The GitHub Actions workflows for automatic deployment are already configured in the repository. You just need to set up the necessary credentials.

### Setup Steps

In order to deploy upon every merge automatically using GitHub Actions:

1. Ensure QA (`qa` branch) multidev environment exists for the given project. This is automatically created during bootstrap, or can be created manually.
1. Double-check if `./.ddev/providers/pantheon.yaml` contains the proper Pantheon project name.
1. Get a [Pantheon machine token](https://pantheon.io/docs/machine-tokens) (using a dummy new Pantheon user ideally, one user per project for the sake of security)
1. Get a GitHub Personal access token. It will be used to post a comment to GitHub to the relevant issue when a merged PR is deployed, so set the expiry date far in the future enough for this.
1. `ddev robo deploy:config-autodeploy [your terminus token] [your github token]`
1. `git commit -m "Deployment secrets and configuration"`
1. Add the public key in `pantheon-key.pub` to the newly created dummy [Pantheon user](https://pantheon.io/docs/ssh-keys)
1. Set up the following in your GitHub repository settings:

**GitHub Secrets** (Settings → Secrets and variables → Actions → Secrets):
- `TERMINUS_TOKEN`: Your Pantheon machine token
- `PANTHEON_DEPLOY_KEY`: The SSH private key for deployment
- `GH_TOKEN`: GitHub personal access token for posting deployment comments
1. Run the autodeploy configuration command:
```bash
ddev robo deploy:config-autodeploy [your terminus token] [your github token]
```

**GitHub Variables** (Settings → Secrets and variables → Actions → Variables):
- `PANTHEON_GIT_URL`: The Pantheon Git URL for your project
- `ROLLBAR_SERVER_TOKEN`: Your Rollbar server token (optional)
- `DEPLOY_EXCLUDE_WARNING`: Warnings to exclude from deployment notifications (optional)

1. Actualize `public static string $githubProject = 'Gizra/the-client';` in the `RoboFile.php`.
This will generate an SSH key pair. The command will automatically install the [GitHub CLI](https://cli.github.com/) (`gh`) if it's not already available in your DDEV environment, then offer to automatically set up GitHub Secrets and Variables. If installation fails, it will provide manual instructions.

1. Follow the instructions provided by the command to:
- Add the SSH public key (`pantheon-key.pub`) to your [Pantheon account](https://pantheon.io/docs/ssh-keys)
- If using automated setup: Confirm when prompted to automatically configure GitHub Secrets and Variables
- If setting up manually: Configure GitHub Secrets (TERMINUS_TOKEN, PANTHEON_DEPLOY_KEY, GH_TOKEN) and Variables (PANTHEON_GIT_URL, ROLLBAR_SERVER_TOKEN, DEPLOY_EXCLUDE_WARNING) as instructed

**Note**: If you used the `bootstrap:project` command to create your project, the `$githubProject` variable in `DeploymentTrait.php` is automatically updated with your organization and project name. Otherwise, you'll need to manually update `public static string $githubProject = 'YourOrg/your-project';` in `robo-components/DeploymentTrait.php`.

Optionally you can specify which target branch you'd like to push on Pantheon, by default it's `master`, so the target is the DEV environment, but alternatively you can issue:
`ddev robo deploy:config-autodeploy [your terminus token] [your github token] [pantheon project name] [gh_branch] [pantheon_branch]`
### Tag-based Deployments

After you have automatic deployment for a project, you are able to deploy to Pantheon `test` and `live` using Git tags.
`git tag 0.1.2` will imply a deployment to the `test` environment (and `dev` - as enforced by Pantheon).
`git tag 0.1.2_live` will imply a deployment to `live`. In order to make it fast, you need to first create the tag that deploy to `test`, then you need to tag the same commit with a tag suffixed with `_live`.
- `git tag 0.1.2` will imply a deployment to the `test` environment (and `dev` - as enforced by Pantheon).
- `git tag 0.1.2_live` will imply a deployment to `live`. In order to make it fast, you need to first create the tag that deploy to `test`, then you need to tag the same commit with a tag suffixed with `_live`.

### Excluding Warnings in Deployment

Expand Down
146 changes: 91 additions & 55 deletions robo-components/BootstrapTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ trait BootstrapTrait {
* Bootstrap a new client project on Pantheon.io.
*
* @param string $project_name
* The project name.
* The Pantheon project name.
* @param string $github_repository_url
* The clone URL of the GitHub repository.
* @param string $terminus_token
Expand All @@ -27,52 +27,63 @@ trait BootstrapTrait {
* The HTTP basic auth password. Optional.
*/
public function bootstrapProject(string $project_name, string $github_repository_url, string $terminus_token, string $github_token, string $http_basic_auth_user = '', string $http_basic_auth_password = '') {
// Extract project name from $github_repository_url.
// The syntax is like: git@github.com:Organization/projectname.git .
// Extract organization and repo name from GitHub URL.
preg_match('/github.com[:\/](.*)\/(.*)\.git/', $github_repository_url, $matches);
$github_organization = $matches[1];
$project_machine_name = $matches[2];
$github_repo_name = $matches[2];

$this->verifyRequirements($project_name, $github_organization, $project_machine_name, $terminus_token, $github_token, $http_basic_auth_user, $http_basic_auth_password);
$this->verifyRequirements($project_name, $github_organization, $github_repo_name, $terminus_token, $github_token, $http_basic_auth_user, $http_basic_auth_password);

$this->prepareGithubRepository($project_name, $github_organization, $project_machine_name, $github_repository_url);
$this->prepareGithubRepository($project_name, $github_organization, $github_repo_name, $github_repository_url);

$this->createPantheonProject($terminus_token, $project_name, $project_machine_name);
$this->createPantheonProject($terminus_token, $project_name);

$this->deployPantheonInstallEnv('dev', $project_machine_name);
$this->deployPantheonInstallEnv('qa', $project_machine_name);
$this->deployPantheonInstallEnv('dev', $project_name);
$this->deployPantheonInstallEnv('qa', $project_name);

$this->lockPantheonEnvironments($project_machine_name, $http_basic_auth_user, $http_basic_auth_password);
$this->lockPantheonEnvironments($project_name, $http_basic_auth_user, $http_basic_auth_password);

$tfa_secret = $this->taskExec("openssl rand -base64 32")
->printOutput(FALSE)
->run()
->getMessage();
$this->taskExec('terminus self:plugin:install pantheon-systems/terminus-secrets-plugin')->run();
$this->taskExec("terminus secrets:set $project_machine_name.qa tfa $tfa_secret")->run();
$this->taskExec("terminus secrets:set $project_machine_name.dev tfa $tfa_secret")->run();
$this->taskExec("terminus secrets:set $project_name.qa tfa $tfa_secret")->run();
$this->taskExec("terminus secrets:set $project_name.dev tfa $tfa_secret")->run();

$this->say("Bootstrap completed successfully.");
$this->say("You might want to run the following commands to properly place the project:");
$this->say("mv .bootstrap ../$project_machine_name");
$this->say("mv .pantheon ../$project_machine_name/.pantheon");
$this->say("To configure autodeployment to Pantheon run:");
$this->say("ddev robo deploy:config-autodeploy $terminus_token $github_token");
$this->say("");
$this->say("Next steps:");
$this->say("1. Move the project to its final location:");
$this->say(" mv .bootstrap ../$github_repo_name");
$this->say(" mv .pantheon ../$github_repo_name/.pantheon");
$this->say("");
$this->say("2. Configure automatic deployment to Pantheon with GitHub Actions:");
$this->say(" cd ../$github_repo_name");
$this->say(" ddev robo deploy:config-autodeploy $terminus_token $github_token");
$this->say("");
$this->say(" This will generate SSH keys and provide instructions for:");
$this->say(" - Setting up GitHub Secrets (TERMINUS_TOKEN, PANTHEON_DEPLOY_KEY, GH_TOKEN)");
$this->say(" - Setting up GitHub Variables (PANTHEON_GIT_URL, ROLLBAR_SERVER_TOKEN)");
$this->say(" - Adding the SSH public key to your Pantheon account");
$this->say("");
$this->say("For full deployment setup details, see:");
$this->say("https://github.com/$github_organization/$github_repo_name#automatic-deployment-to-pantheon");
}

/**
* Prepares the new GitHub repository for the project.
*
* @param string $project_name
* The project name.
* @param string $organization
* The Pantheon site name.
* @param string $github_organization
* The GitHub organization.
* @param string $project_machine_name
* The project machine name in GH slug.
* @param string $github_repo_name
* The GitHub repository name.
* @param string $github_repository_url
* The clone URL of the GitHub repository.
*/
protected function prepareGithubRepository(string $project_name, string $organization, string $project_machine_name, string $github_repository_url) {
protected function prepareGithubRepository(string $project_name, string $github_organization, string $github_repo_name, string $github_repository_url) {
$temp_remote = 'bootstrap_' . time();
$this->taskExec("git remote add $temp_remote $github_repository_url")
->run();
Expand All @@ -94,12 +105,12 @@ protected function prepareGithubRepository(string $project_name, string $organiz

$this->taskReplaceInFile('.bootstrap/robo-components/DeploymentTrait.php')
->from('Gizra/drupal-starter')
->to("$organization/$project_machine_name")
->to("$github_organization/$github_repo_name")
->run();

$this->taskReplaceInFile('.bootstrap/.ddev/config.yaml')
->from('drupal-starter')
->to($project_machine_name)
->to($github_repo_name)
->run();

$this->taskReplaceInFile('.bootstrap/.ddev/config.yaml')
Expand All @@ -124,12 +135,12 @@ protected function prepareGithubRepository(string $project_name, string $organiz

$this->taskReplaceInFile('.bootstrap/README.md')
->from('Gizra')
->to($organization)
->to($github_organization)
->run();

$this->taskReplaceInFile('.bootstrap/README.md')
->from('drupal-starter')
->to($project_machine_name)
->to($github_repo_name)
->run();

$this->taskReplaceInFile('.bootstrap/.ddev/providers/pantheon.yaml')
Expand All @@ -139,18 +150,28 @@ protected function prepareGithubRepository(string $project_name, string $organiz

$this->taskReplaceInFile('.bootstrap/composer.json')
->from('drupal-starter')
->to(strtolower($project_machine_name))
->to(strtolower($github_repo_name))
->run();
$this->taskReplaceInFile('.bootstrap/composer.json')
->from('gizra')
->to(strtolower($organization))
->to(strtolower($github_organization))
->run();

$this->taskReplaceInFile('.bootstrap/web/sites/default/settings.pantheon.php')
->from('drupal_starter')
->to(str_replace('-', '_', $project_machine_name))
->to(str_replace('-', '_', $github_repo_name))
->run();

// Run composer install first to get contrib modules (needed for
// merge-plugin to find webform's composer.libraries.json).
$result = $this->taskExec("cd .bootstrap && composer install --no-interaction")
->run()
->getExitCode();
if ($result !== 0) {
throw new \Exception("Failed to run composer install in GH repository.");
}

// Now update the lock file hash after the project name replacements.
$result = $this->taskExec("cd .bootstrap && composer update --lock")
->run()
->getExitCode();
Expand Down Expand Up @@ -178,11 +199,9 @@ protected function prepareGithubRepository(string $project_name, string $organiz
* @param string $terminus_token
* The Pantheon machine token.
* @param string $project_name
* The project name.
* @param string $project_machine_name
* The project machine name in GH slug.
* The Pantheon site name.
*/
public function createPantheonProject(string $terminus_token, string $project_name, string $project_machine_name) {
public function createPantheonProject(string $terminus_token, string $project_name) {
$result = $this->taskExec("terminus auth:login --machine-token=\"$terminus_token\"")
->run()
->getExitCode();
Expand Down Expand Up @@ -213,15 +232,15 @@ public function createPantheonProject(string $terminus_token, string $project_na
// matches Drupal Starter.
$upstream_id = "bde48795-b16d-443f-af01-8b1790caa1af";

$result = $this->taskExec("terminus site:create $project_machine_name \"$project_name\" \"$upstream_id\" --org=\"$selected_organization_id\"")
$result = $this->taskExec("terminus site:create $project_name \"$project_name\" \"$upstream_id\" --org=\"$selected_organization_id\"")
->run()
->getExitCode();

if ($result !== 0) {
throw new \Exception("Failed to create the Pantheon project.");
}

$result = $this->taskExec("terminus connection:set $project_machine_name.dev git")
$result = $this->taskExec("terminus connection:set $project_name.dev git")
->run()
->getExitCode();

Expand All @@ -231,7 +250,7 @@ public function createPantheonProject(string $terminus_token, string $project_na

// Retrieve Git repository from Pantheon, then clone the artifact repository
// to .pantheon directory.
$pantheon_repository_url = $this->taskExec("terminus connection:info $project_machine_name.dev --field=git_url")
$pantheon_repository_url = $this->taskExec("terminus connection:info $project_name.dev --field=git_url")
->printOutput(FALSE)
->run()
->getMessage();
Expand Down Expand Up @@ -297,15 +316,15 @@ public function createPantheonProject(string $terminus_token, string $project_na
}

// Create QA environment on Pantheon.
$result = $this->taskExec("terminus multidev:create $project_machine_name.dev qa")
$result = $this->taskExec("terminus multidev:create $project_name.dev qa")
->run()
->getExitCode();

if ($result !== 0) {
throw new \Exception('Failed to create the Pantheon QA environment.');
}

$result = $this->taskExec("terminus connection:set $project_machine_name.qa git")
$result = $this->taskExec("terminus connection:set $project_name.qa git")
->run()
->getExitCode();

Expand All @@ -317,33 +336,33 @@ public function createPantheonProject(string $terminus_token, string $project_na
/**
* Lock all Pantheon environments for the given site.
*
* @param string $project_machine_name
* The machine name of the project.
* @param string $project_name
* The Pantheon site name.
* @param string $http_basic_auth_user
* The HTTP basic auth user.
* @param string $http_basic_auth_password
* The HTTP basic auth password.
*/
public function lockPantheonEnvironments(string $project_machine_name, string $http_basic_auth_user, string $http_basic_auth_password) {
public function lockPantheonEnvironments(string $project_name, string $http_basic_auth_user, string $http_basic_auth_password) {
if (empty($http_basic_auth_user) || empty($http_basic_auth_password)) {
$this->say("No HTTP basic auth credentials were provided. Pantheon environments will not be locked.");
return;
}
$pantheon_environments = $this->taskExec("terminus env:list $project_machine_name --field=ID --format=list")
$pantheon_environments = $this->taskExec("terminus env:list $project_name --field=ID --format=list")
->printOutput(FALSE)
->run()
->getMessage();

$pantheon_environments = explode(PHP_EOL, $pantheon_environments);
foreach ($pantheon_environments as $pantheon_environment) {
$result = $this->taskExec("terminus env:wake $project_machine_name.$pantheon_environment")
$result = $this->taskExec("terminus env:wake $project_name.$pantheon_environment")
->run()
->getExitCode();
if ($result !== 0) {
$this->say("Failed to wake up the Pantheon $pantheon_environment environment.");
continue;
}
$result = $this->taskExec("terminus lock:enable $project_machine_name.$pantheon_environment $http_basic_auth_user $http_basic_auth_password")
$result = $this->taskExec("terminus lock:enable $project_name.$pantheon_environment $http_basic_auth_user $http_basic_auth_password")
->run()
->getExitCode();
if ($result !== 0) {
Expand All @@ -357,10 +376,10 @@ public function lockPantheonEnvironments(string $project_machine_name, string $h
*
* @param string $project_name
* The project name.
* @param string $organization
* @param string $github_organization
* The GitHub organization.
* @param string $project_machine_name
* The project machine name in GH slug.
* @param string $github_repo_name
* The GitHub repository name.
* @param string $terminus_token
* The Pantheon machine token.
* @param string $github_token
Expand All @@ -370,7 +389,7 @@ public function lockPantheonEnvironments(string $project_machine_name, string $h
* @param string $http_basic_auth_password
* The HTTP basic auth password.
*/
protected function verifyRequirements(string $project_name, string $organization, string $project_machine_name, string $terminus_token, string $github_token, $http_basic_auth_user, $http_basic_auth_password) {
protected function verifyRequirements(string $project_name, string $github_organization, string $github_repo_name, string $terminus_token, string $github_token, $http_basic_auth_user, $http_basic_auth_password) {
if (is_dir('.bootstrap')) {
throw new \Exception('The .bootstrap directory already exists. Please remove / move it and try again.');
}
Expand All @@ -380,14 +399,13 @@ protected function verifyRequirements(string $project_name, string $organization
if (empty(trim($project_name))) {
throw new \Exception('The project name is empty.');
}
if (empty(trim($organization))) {
throw new \Exception('The organization is empty.');
}
if (empty(trim($project_machine_name))) {
throw new \Exception('The project machine name is empty.');
$this->validatePantheonSiteName($project_name);

if (empty(trim($github_organization))) {
throw new \Exception('The GitHub organization is empty.');
}
if (str_contains($project_machine_name, ' ')) {
throw new \Exception('The project machine name contains spaces.');
if (empty(trim($github_repo_name))) {
throw new \Exception('The GitHub repository name is empty.');
}
if (empty(trim($terminus_token))) {
throw new \Exception('The Pantheon machine token is empty.');
Expand All @@ -397,4 +415,22 @@ protected function verifyRequirements(string $project_name, string $organization
}
}

/**
* Validates a Pantheon site name.
*
* @param string $site_name
* The site name to validate.
*
* @throws \Exception
* If the site name is invalid.
*/
protected function validatePantheonSiteName(string $site_name): void {
if (strlen($site_name) >= 52) {
throw new \Exception("The site name '$site_name' must be fewer than 52 characters.");
}
if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$|^[a-zA-Z0-9]$/', $site_name)) {
throw new \Exception("The site name '$site_name' can only contain a-z, A-Z, 0-9, and dashes, and cannot begin or end with a dash.");
}
}

}
Loading
Loading