Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9912c38
fix: Performance improvements for PantheonReviewApps
rabbitlair Apr 15, 2026
78071dd
feat: add AsyncDeploy option for Pantheon review apps via Quicksilver…
rabbitlair Apr 15, 2026
e500d43
tests: Add some tests for the Pantheon AsyncDeploy option
rabbitlair Apr 15, 2026
9f124f4
tests: fix tests for async deploy
rabbitlair Apr 15, 2026
aa029bb
chore: address zizmor findings and other minor fixes
rabbitlair Apr 15, 2026
041bda2
Improve Pantheon async deploy observability
rabbitlair Apr 16, 2026
4f2b8fe
tests: fix some Yarn tests
rabbitlair Apr 16, 2026
29e4c82
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair Apr 20, 2026
f693306
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair Apr 20, 2026
f372416
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair Apr 20, 2026
abd1aad
chore: allow tests for Yarn 3 and 4 to run
rabbitlair Apr 20, 2026
db42916
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair May 19, 2026
3fb3ab4
fix: use env vars instead of secrets context in step if condition
rabbitlair May 19, 2026
32bcb25
fix: persist SSH agent across steps in setup-ssh action
rabbitlair May 20, 2026
d089dea
Dummy commit to trigger CI builds
rabbitlair May 20, 2026
6fb9414
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair May 25, 2026
e399d1d
Revert unrelated security changes
rabbitlair May 25, 2026
0af22ae
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair May 25, 2026
6de1b9a
Pantheon async deploy via Quicksilver with terminus caching, watchdog…
rabbitlair May 25, 2026
10ca3cc
Fix failing tests
rabbitlair May 25, 2026
f19fbe8
Keep yargs package in 18.0.0 version, same as main
rabbitlair May 25, 2026
5c3ac86
Fix javascript tests
rabbitlair May 25, 2026
fb2a4c0
Implement feature parity for Gitlab's Pantheon Review Apps (async)
rabbitlair May 26, 2026
64d33f2
Extract Pantheon deploy logic into shared Taskfile tasks and wire up …
rabbitlair May 26, 2026
cad1710
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair May 28, 2026
fd2a272
Fix test for Pantheon Github Actions
rabbitlair May 29, 2026
ad74ba7
Ensure Terminus can be installed within DDEV
rabbitlair May 29, 2026
16298f5
Ensure TestGitHubActions workflow runs properly
rabbitlair Jun 1, 2026
4f98d24
Surface Pantheon tasks errors to the logs
rabbitlair Jun 1, 2026
944ab89
Fix error after deploying to Pantheon
rabbitlair Jun 1, 2026
7466aa0
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair Jun 4, 2026
597bd41
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair Jun 4, 2026
55a2811
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair Jun 25, 2026
40e19df
Merge branch '553--pantheon-review-apps-performance' of github.com:Lu…
rabbitlair Jun 25, 2026
252cef9
Merge branch 'main' into 553--pantheon-review-apps-performance
rabbitlair Jun 30, 2026
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
415 changes: 415 additions & 0 deletions .github/workflows/TestPantheonAsyncDeploy.yml

Large diffs are not rendered by default.

172 changes: 171 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ To enable deployment of Pantheon Review Apps (Multidev environments per pull req
- `TERMINUS_PLUGINS` (optional) Comma-separated list of Terminus plugins to be available
- `TERMINUS_TIMEOUT_LIMIT` (optional) Number of seconds that terminus will wait until timeout. Defaults to 600
- `PANTHEON_CLONE_FROM` (optional) The environment to clone from when creating multidev sites. Defaults to 'live'
- `PANTHEON_SKIP_WIPE_MULTIDEV` (optional) Set to 'true' to skip wiping the multidev environment on each push, preserving its database state. Defaults to 'false'
- `PANTHEON_SKIP_WIPE_MULTIDEV` (optional) Set to `'true'` to skip wiping and re-cloning the multidev's database and files on each push. When the multidev does not yet exist it is still created with a full content clone from the source environment; only subsequent pushes skip the clone. Defaults to `'false'` (always wipe and re-clone). See [Multidev content cloning performance](#multidev-content-cloning-performance).
- Add the following [secrets to your GitHub repository](https://docs.github.com/en/codespaces/managing-codespaces-for-your-organization/managing-development-environment-secrets-for-your-repository-or-organization#adding-secrets-for-a-repository):
- `TERMINUS_MACHINE_TOKEN` See https://pantheon.io/docs/terminus/install#machine-token (`PANTHEON_TERMINUS_TOKEN` is also accepted for backwards compatibility)
- `SSH_PRIVATE_KEY` A private key of a user which can push to Pantheon
Expand All @@ -742,6 +742,97 @@ To enable deployment of Pantheon Review Apps (Multidev environments per pull req
> `"github": {"pantheon": ["ReviewApps"]}`. Similarly, `"github": ["Pantheon"]` (previously a
> no-op) is now a deprecated alias for `"github": {"pantheon": ["Actions"]}`.

### Async deploy with Quicksilver

By default, the review app job blocks the CI runner for up to 10 minutes while it waits for
Pantheon to finish syncing code (`terminus workflow:wait`). The `AsyncDeploy` option eliminates
this wait by splitting the job in two:

- **Job 1 (Push):** builds the project, creates the multidev, pushes code to Pantheon, then exits
immediately. The CI runner is released within a few minutes. The GitHub Deployment remains
`in_progress`.
- **Pantheon** syncs code internally and fires the `sync_code` Quicksilver hook.
- **Quicksilver script** (PHP running on Pantheon): detects the `pr-NNN` multidev pattern, reads
secrets from Pantheon Secrets, and fires a `repository_dispatch` event to GitHub.
- **Job 2 (Post-deploy):** triggered by the webhook — runs `terminus aliases`, `drush update` (or
`task update`), and marks the GitHub Deployment as `success`.

#### Enabling async deploy

Add `"AsyncDeploy"` alongside `"ReviewApps"` in your `composer.json`:

```json
"extra": {
"drainpipe": {
"github": {
"pantheon": ["ReviewApps", "AsyncDeploy"]
}
}
}
```

Then run `composer install`. The following files are scaffolded or replaced:

- `.github/workflows/PantheonReviewApps.yml` — replaced with the async variant (exits after git push)
- `.github/workflows/PantheonReviewAppsPostDeploy.yml` — new post-deploy workflow triggered by Quicksilver
- `web/private/scripts/drainpipe_notify_github.php` — Quicksilver script that fires the webhook

#### Manual step: add the Quicksilver hook to `pantheon.yml`

Drainpipe does not auto-edit `pantheon.yml` because it is version-controlled and site-specific.
Add the following to your site's `pantheon.yml`:

```yaml
workflows:
sync_code:
after:
- type: webphp
description: Notify CI after Pantheon code sync
script: private/scripts/drainpipe_notify_github.php
```

> **Important:** the path is `private/scripts/` — **not** `web/private/scripts/`. Pantheon resolves
> this path relative to the web root when `web_docroot: true` is set. Using the `web/` prefix is a
> common misconfiguration that silently prevents the hook from running.

#### Set Pantheon secrets (once per site)

The Quicksilver script reads credentials from Pantheon Secrets at runtime using
`pantheon_get_secret()`. Set them with Terminus before the first deployment:

```bash
terminus secret:set <site> drainpipe_github_token <PAT-with-repo-scope>
terminus secret:set <site> drainpipe_github_repo owner/repo
```

> **Note:** `GITHUB_TOKEN` (the automatic Actions token) cannot be used here because Quicksilver
> runs outside GitHub's infrastructure. You must create a [personal access token](https://github.com/settings/tokens)
> with `repo` scope.

> **Note:** `terminus secret:set` requires the
> [`terminus-secrets-manager-plugin`](https://github.com/pantheon-systems/terminus-secrets-manager-plugin)
> to be installed wherever Terminus runs (CI runner or local workstation). You can make it
> available in CI by adding it to the `TERMINUS_PLUGINS` repository variable.

#### GitHub checks behaviour

This is an important difference from the default blocking mode:

| | Blocking mode | Async mode |
|---|---|---|
| **Job 1** | Appears as a PR check; can block merging | Appears as a PR check; can block merging |
| **Job 2** | N/A (single job) | **Does NOT appear as a PR check** — only visible in the GitHub Deployments panel (environment badge) and the Actions tab |

In async mode, a failure in Job 2 (e.g. `drush update` fails) will **not** block a merge even if
the review app check is set as required in branch protection. Monitor the Deployments panel on
the PR to confirm Job 2 succeeds before merging.

#### Concurrency

If a developer pushes twice in quick succession, a second Quicksilver event fires while Job 2 is
still running. The `cancel-in-progress: true` concurrency group on the post-deploy workflow cancels
the older Job 2 automatically — only the latest sync triggers a Drush update.

### Acquia

To add Acquia specific GitHub actions, add the following to `composer.json`:
Expand Down Expand Up @@ -899,6 +990,7 @@ To enable deployment of Pantheon Review Apps (Multidev environments per merge re
- `TERMINUS_PLUGINS` (optional) Comma-separated list of Terminus plugins to be available
- `REVIEW_APP_BASIC_AUTH` (optional) Basic auth credentials prepended to the review app URL e.g. `user:password@`
- `PANTHEON_MULTIDEV_RUN_INSTALLER` (optional) Set to `"true"` to run `site:install --existing-config` instead of `drupal:update` when deploying
- `PANTHEON_SKIP_WIPE_MULTIDEV` (optional) Set to `"true"` to skip deleting and re-creating the multidev on each push, preserving its database and files state. When the multidev does not yet exist it is still created with a full content clone from the source environment; only subsequent pushes skip the clone. Defaults to `"false"` (always delete and re-create). See [Multidev content cloning performance](#multidev-content-cloning-performance).

This will setup Merge Request deployment to Pantheon Multidev environments. See
[scaffold/gitlab/gitlab-ci.example.yml] for an example. You can also just
Expand All @@ -910,6 +1002,84 @@ such as setting up [Terminus](https://pantheon.io/docs/terminus). See
> Migrate to `"gitlab": {"pantheon": ["Deploy", "ReviewApps"]}`.`"gitlab": ["Pantheon"]` alone
> maps to `{"pantheon": ["Deploy"]}`.

### Async deploy with Quicksilver (GitLab)

The `AsyncDeploy` option is also available for GitLab. Add it alongside `"ReviewApps"`:

```json
"extra": {
"drainpipe": {
"gitlab": {
"pantheon": ["Deploy", "ReviewApps", "AsyncDeploy"]
}
}
}
```

Then run `composer install`. The following files are scaffolded or replaced:

- `.drainpipe/gitlab/PantheonReviewApps.gitlab-ci.yml` — replaced with the async variant (exits after git push)
- `.drainpipe/gitlab/PantheonReviewAppsPostDeploy.gitlab-ci.yml` — new post-deploy job triggered by Quicksilver
- `web/private/scripts/drainpipe_notify_gitlab.php` — Quicksilver script that fires the pipeline trigger

#### Manual step: add the Quicksilver hook to `pantheon.yml`

Drainpipe does not auto-edit `pantheon.yml` because it is version-controlled and site-specific.
Add the following to your site's `pantheon.yml`:

```yaml
workflows:
sync_code:
after:
- type: webphp
description: Notify CI after Pantheon code sync
script: private/scripts/drainpipe_notify_gitlab.php
```

> **Important:** the path is `private/scripts/` — **not** `web/private/scripts/`. Pantheon resolves
> this path relative to the web root when `web_docroot: true` is set. Using the `web/` prefix is a
> common misconfiguration that silently prevents the hook from running.

#### Set Pantheon secrets (once per site)

```bash
terminus secret:set <site> drainpipe_gitlab_url https://gitlab.com
terminus secret:set <site> drainpipe_gitlab_project_id <numeric-project-id>
terminus secret:set <site> drainpipe_gitlab_trigger_token <pipeline-trigger-token>
terminus secret:set <site> drainpipe_gitlab_ref main
```

> **Note:** Job 2 (post-deploy) is triggered via a pipeline trigger and runs on the `main` branch,
> not the MR branch. This is intentional — Job 2 only needs `composer install` to obtain the
> `drush`/`task` binaries; it does not rebuild the project. Do not expect Job 2 to run on the MR
> branch.

> **Note:** `terminus secret:set` requires the
> [`terminus-secrets-manager-plugin`](https://github.com/pantheon-systems/terminus-secrets-manager-plugin).
> Add it to the `TERMINUS_PLUGINS` CI variable to make it available in CI.

### Multidev content cloning performance

By default, every push to an open pull request / merge request wipes the multidev's database and
files and re-clones them from the source environment (e.g. `live`). This guarantees that reviewers
always see content that mirrors production, but it is the slowest part of the deployment: cloning a
large database can add several minutes to every CI run.

Set `PANTHEON_SKIP_WIPE_MULTIDEV` to `'true'` (GitHub) or `"true"` (GitLab) to change this
behaviour:

- **First push** (multidev does not exist yet): content is still cloned from the source environment
so the environment starts with realistic data.
- **Subsequent pushes** (multidev already exists): the wipe / delete-and-recreate step is skipped.
Only code is updated. The database and files remain as they were after the previous deployment,
which may include data entered or modified by reviewers.

**When to use it:** `PANTHEON_SKIP_WIPE_MULTIDEV=true` is a good default for projects where
reviewers do not depend on a fresh production database for each review, or where the `live`
database is large enough to make cloning noticeably slow. Keep it at the default `false` when it
is important that each push reflects the current production content (e.g. for content-heavy sites
where QA involves checking migrations or editor workflows).

## Tugboat

Add the following to `composer.json` to add Tugboat configuration:
Expand Down
39 changes: 23 additions & 16 deletions scaffold/github/actions/pantheon/review/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ inputs:
description: "Whether or not to skip wiping the multidev on each push. Defaults to false."
required: false
default: "false"
async:
description: "When 'true', skip the post-deploy step (terminus workflow:wait, drush update, deployment success). The Quicksilver async flow handles that in a separate job."
Comment thread
rabbitlair marked this conversation as resolved.
Outdated
required: false
default: "false"
runs:
using: "composite"
steps:
Expand Down Expand Up @@ -176,28 +180,31 @@ runs:
INPUT_RUN_INSTALLER: ${{ inputs.run-installer }}
INPUT_LOCK_USERNAME: ${{ inputs.lock-username }}
INPUT_LOCK_PASSWORD: ${{ inputs.lock-password }}
INPUT_ASYNC: ${{ inputs.async }}
GITHUB_TOKEN: ${{ inputs.github-token }}
run: |
source .github/actions/drainpipe/set-env/bash_aliases

echo "::notice::Waiting $INPUT_TERMINUS_TIMEOUT seconds for Pantheon to sync..."
drainpipe_exec "terminus workflow:wait -vv --max $INPUT_TERMINUS_TIMEOUT -- $INPUT_SITE_NAME.$MULTIDEV_NAME"
if [ "$INPUT_ASYNC" != "true" ]; then
echo "::notice::Waiting $INPUT_TERMINUS_TIMEOUT seconds for Pantheon to sync..."
drainpipe_exec "terminus workflow:wait -vv --max $INPUT_TERMINUS_TIMEOUT -- $INPUT_SITE_NAME.$MULTIDEV_NAME"

drainpipe_exec "terminus aliases --only $INPUT_SITE_NAME --yes"
if [ "$INPUT_RUN_INSTALLER" == "true" ]; then
drainpipe_exec "./vendor/bin/drush @$INPUT_SITE_NAME.$MULTIDEV_NAME --yes site:install --existing-config"
elif drainpipe_exec "./vendor/bin/task -l | grep '* update: '"; then
drainpipe_exec "./vendor/bin/task update site=@$INPUT_SITE_NAME.$MULTIDEV_NAME"
else
drainpipe_exec "./vendor/bin/task drupal:update site=@$INPUT_SITE_NAME.$MULTIDEV_NAME"
fi
ENVIRONMENT_URL="https://$MULTIDEV_NAME-$INPUT_SITE_NAME.pantheonsite.io"
# Lock Environment.
if [ "$INPUT_LOCK_USERNAME" != "" ] && [ "$INPUT_LOCK_PASSWORD" != "" ]; then
drainpipe_exec "terminus lock:enable $INPUT_SITE_NAME.$MULTIDEV_NAME $INPUT_LOCK_USERNAME $INPUT_LOCK_PASSWORD"
ENVIRONMENT_URL="https://$INPUT_LOCK_USERNAME:$INPUT_LOCK_PASSWORD@$MULTIDEV_NAME-$INPUT_SITE_NAME.pantheonsite.io"
drainpipe_exec "terminus aliases --only $INPUT_SITE_NAME --yes"
if [ "$INPUT_RUN_INSTALLER" == "true" ]; then
drainpipe_exec "./vendor/bin/drush @$INPUT_SITE_NAME.$MULTIDEV_NAME --yes site:install --existing-config"
elif drainpipe_exec "./vendor/bin/task -l | grep '* update: '"; then
drainpipe_exec "./vendor/bin/task update site=@$INPUT_SITE_NAME.$MULTIDEV_NAME"
else
drainpipe_exec "./vendor/bin/task drupal:update site=@$INPUT_SITE_NAME.$MULTIDEV_NAME"
fi
ENVIRONMENT_URL="https://$MULTIDEV_NAME-$INPUT_SITE_NAME.pantheonsite.io"
# Lock Environment.
if [ "$INPUT_LOCK_USERNAME" != "" ] && [ "$INPUT_LOCK_PASSWORD" != "" ]; then
drainpipe_exec "terminus lock:enable $INPUT_SITE_NAME.$MULTIDEV_NAME $INPUT_LOCK_USERNAME $INPUT_LOCK_PASSWORD"
ENVIRONMENT_URL="https://$INPUT_LOCK_USERNAME:$INPUT_LOCK_PASSWORD@$MULTIDEV_NAME-$INPUT_SITE_NAME.pantheonsite.io"
fi
curl -f -H "Authorization: token $GITHUB_TOKEN" -X POST -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/$GITHUB_REPOSITORY/deployments/$GITHUB_DEPLOYMENT_ID/statuses -d "{\"state\":\"success\", \"environment_url\": \"$ENVIRONMENT_URL\"}"
fi
curl -f -H "Authorization: token $GITHUB_TOKEN" -X POST -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/$GITHUB_REPOSITORY/deployments/$GITHUB_DEPLOYMENT_ID/statuses -d "{\"state\":\"success\", \"environment_url\": \"$ENVIRONMENT_URL\"}"
shell: bash

- name: Set Deployment Failure Status
Expand Down
18 changes: 14 additions & 4 deletions scaffold/github/actions/pantheon/setup-terminus/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,24 @@ inputs:
runs:
using: "composite"
steps:
- env:
- name: Cache Terminus
id: terminus-cache
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: ~/terminus/terminus
key: terminus-4.1.1
Comment thread
rabbitlair marked this conversation as resolved.

- name: Install and authenticate Terminus
env:
INPUT_TERMINUS_TOKEN: ${{ inputs.terminus-token }}
INPUT_TERMINUS_PLUGINS: ${{ inputs.terminus-plugins }}
run: |
echo -e "Host *.drush.in\n StrictHostKeyChecking no\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa" >> ~/.ssh/config
mkdir ~/terminus
curl -L https://github.com/pantheon-systems/terminus/releases/download/4.1.1/terminus.phar --output ~/terminus/terminus
chmod +x ~/terminus/terminus
mkdir -p ~/terminus
if [ "${{ steps.terminus-cache.outputs.cache-hit }}" != "true" ]; then
curl -L https://github.com/pantheon-systems/terminus/releases/download/4.1.1/terminus.phar --output ~/terminus/terminus
Comment thread
rabbitlair marked this conversation as resolved.
chmod +x ~/terminus/terminus
fi
ln -s ~/terminus/terminus /usr/local/bin/terminus
Comment thread
rabbitlair marked this conversation as resolved.
Outdated
echo "$INPUT_TERMINUS_PLUGINS" | while read -d, plugin || [[ -n $plugin ]]; do terminus self:plugin:install $plugin; done
terminus auth:login --machine-token="$INPUT_TERMINUS_TOKEN"
Expand Down
11 changes: 9 additions & 2 deletions scaffold/github/actions/pantheon/update/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ inputs:
environment:
description: "The environment to run the updates or install in e.g. 'test'"
required: true
skip-workflow-wait:
description: "When 'true', skip terminus workflow:wait. Use when the sync is already complete (e.g. triggered by Quicksilver)."
required: false
default: "false"
runs:
using: "composite"
steps:
Expand All @@ -18,10 +22,13 @@ runs:
INPUT_SITE_NAME: ${{ inputs.site-name }}
INPUT_ENVIRONMENT: ${{ inputs.environment }}
INPUT_RUN_INSTALLER: ${{ inputs.run-installer }}
INPUT_SKIP_WORKFLOW_WAIT: ${{ inputs.skip-workflow-wait }}
run: |
# Wait for Pantheon to sync.
source .github/actions/drainpipe/set-env/bash_aliases
drainpipe_exec "terminus workflow:wait $INPUT_SITE_NAME.$INPUT_ENVIRONMENT"
if [ "$INPUT_SKIP_WORKFLOW_WAIT" != "true" ]; then
# Wait for Pantheon to sync.
drainpipe_exec "terminus workflow:wait $INPUT_SITE_NAME.$INPUT_ENVIRONMENT"
fi
drainpipe_exec "terminus aliases --only $INPUT_SITE_NAME --yes"
if [ "$INPUT_RUN_INSTALLER" == "true" ]; then
drainpipe_exec "./vendor/bin/drush @$INPUT_SITE_NAME.$INPUT_ENVIRONMENT --yes site:install --existing-config"
Expand Down
Loading
Loading