Ever felt like getting a Drupal site deployed is more complicated than it needs to be? You want things to be consistent, fast, and smooth, but sometimes it just feels like one wrong step could mess up the whole process. That's exactly where Continuous Integration (CI) and Continuous Deployment (CD) pipelines come in, and when you combine them with the power of Drush, everything clicks into place.
Today, we're going to dig into how we can set up a really solid CI/CD pipeline using GitHub Actions. This isn't just about pushing code; it's about letting Drush handle the tough parts of managing your site and database automatically.
Why Bother with CI/CD for Drupal?
Before we jump into the "how," let's quickly chat about why CI/CD is such a big deal for Drupal projects:
- No More Guesswork: Every deployment follows the exact same steps, every single time. This means fewer "oops, I forgot a step" moments.
- Lightning-Fast Deployments: Tasks that used to take ages now happen in minutes. Efficiency is key!
- Solid Reliability: Automated steps build consistency, leading to more stable deployments with fewer unexpected problems.
- Less Downtime, More Uptime: We'll show you how to manage maintenance mode gracefully, keeping your site running smoothly for your users.
- Easy Rollbacks: If something does go wrong (because, let's be honest, it happens), you can quickly switch back to a working version.
Drush: Your Essential Tool in the CI/CD Process
Drush is essentially your command-line interface for Drupal. It lets you do a ton of things directly from your terminal, making it super useful for automation. Think of it as your go-to helper for your CI/CD setup.
Drush commands can:
- Put your site into maintenance mode.
- Update your database (drush updb).
- Import your latest configuration changes (drush cim).
- Clear out those stubborn caches (drush cr).
- Handle all sorts of site settings and states.
By including these commands right within your CI/CD workflow, you're turning manual, error-prone steps into reliable, repeatable actions. It's truly a game-changer!
A Closer Look At My GitHub Actions Workflow
Here's the GitHub Actions file that I'm using. I'll break down how each part works to get your Drupal site deployed smoothly. This workflow kicks off whenever I create a new "release" in GitHub, which is a smart way to make sure only tested, stable versions hit your live site. Is it perfect? Far from it. Does it work? Absolutely! I've been using this deployment pipeline for over a year now.
If you're brand new to GitHub Actions and wondering how to even create one of these files, don't worry! GitHub and the GitHub community have lots of tutorials to get you started. You can find a comprehensive guide on setting up your first GitHub Actions workflow in the official documentation.
My current .github/ci-cd-pipeline.yml
file:
name: CI/CD Pipeline
on:
release:
types: [created]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Set up PHP and Composer
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: none
tools: composer:v2
- name: Install Composer dependencies
run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader
- name: Set release version environment variable
run: echo "RELEASE_VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
- name: Create production artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.RELEASE_VERSION }}
path: |
.
include-hidden-files: true
- name: Download production artifact
uses: actions/download-artifact@v4
with:
name: ${{ env.RELEASE_VERSION }}
path: distfiles/${{ env.RELEASE_VERSION }}
- name: Upload artifact to remote server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.REMOTE_SERVER_HOST }}
port: ${{ secrets.SSH_PORT }}
username: ${{ secrets.REMOTE_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: distfiles/${{ env.RELEASE_VERSION }}/*
target: ${{ secrets.REMOTE_DIRECTORY }}/releases
strip_components: 1
- name: Setup production deployment directory
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.REMOTE_SERVER_HOST }}
username: ${{ secrets.REMOTE_USER }}
port: ${{ secrets.SSH_PORT }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd ${{ secrets.REMOTE_DIRECTORY }}/releases
# Put the site in offline mode and clear the caches using drush.
current/vendor/bin/drush config:set system.maintenance message "Our servers are getting a quick tune-up. Your patience is appreciated!" -y
current/vendor/bin/drush state:set system.maintenance_mode 1 --input-format=integer
current/vendor/bin/drush drush cr
# Set up symlinks to new release.
rm current
ln -s ${{ secrets.REMOTE_DIRECTORY }}/releases/${{ env.RELEASE_VERSION }} current
chmod +x current/vendor/bin/*
ln -s ${{ secrets.REMOTE_DIRECTORY }}/files ${{ secrets.REMOTE_DIRECTORY }}/releases/${{ env.RELEASE_VERSION }}/web/sites/default/files
ln -s ${{ secrets.REMOTE_DIRECTORY }}/settings.php ${{ secrets.REMOTE_DIRECTORY }}/releases/${{ env.RELEASE_VERSION }}/web/sites/default/settings.php
rm ${{ secrets.REMOTE_DIRECTORY }}/public_html
ln -s ${{ secrets.REMOTE_DIRECTORY }}/releases/${{ env.RELEASE_VERSION }}/web ${{ secrets.REMOTE_DIRECTORY }}/public_html
# Run drush commands.
${{ env.RELEASE_VERSION }}/vendor/bin/drush updb -y
${{ env.RELEASE_VERSION }}/vendor/bin/drush cim -y
${{ env.RELEASE_VERSION }}/vendor/bin/drush state:set system.maintenance_mode 0 --input-format=integer
${{ env.RELEASE_VERSION }}/vendor/bin/drush cr
# Keep only the last 5 versions
find ${{ secrets.REMOTE_DIRECTORY }}/releases -mindepth 1 -maxdepth 1 -type d -regex '.*/v[0-9]*\.[0-9]*\.[0-9]*' | grep -v "${{ env.RELEASE_VERSION }}" | sort -t '-' -k 1,1r | tail -n +2 | xargs rm -rf
echo "Server setup completed successfully." >> ${{ secrets.REMOTE_DIRECTORY }}/deployment.log
backup_database:
runs-on: ubuntu-latest
steps:
- name: Create database backup
uses: appleboy/ssh-action@v0.1.3
with:
host: ${{ secrets.REMOTE_SERVER_HOST }}
port: ${{ secrets.SSH_PORT }}
username: ${{ secrets.REMOTE_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd ${{ secrets.REMOTE_DIRECTORY }}
mysqldump -h ${{ secrets.DB_HOST }} -u ${{ secrets.DB_USER }} -p${{ secrets.DB_PASSWORD }} ${{ secrets.DB_DATABASE }} > database_backup.sql
echo "Database backup created successfully." >> ${{ secrets.REMOTE_DIRECTORY }}/deployment.log
rollback:
needs: build-and-deploy
runs-on: ubuntu-latest
if: failure()
steps:
- name: SSH and rollback
uses: appleboy/ssh-action@v0.1.3
with:
host: ${{ secrets.REMOTE_SERVER_HOST }}
port: ${{ secrets.SSH_PORT }}
username: ${{ secrets.REMOTE_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd ${{ secrets.REMOTE_DIRECTORY }}
# Find the previous version
previous_version=$(find ${{ secrets.REMOTE_DIRECTORY }}/releases -type d -mindepth 1 -maxdepth 1 | grep -v "${{ env.RELEASE_VERSION }}" | sort -t '-' -k 1,1r | tail -n 1 | basename)
# Rollback to the previous version
rm ${{ secrets.REMOTE_DIRECTORY }}/releases/current
ln -s ${{ secrets.REMOTE_DIRECTORY }}/releases/${previous_version} ${{ secrets.REMOTE_DIRECTORY }}/releases/current
rm -rf "${{ secrets.REMOTE_DIRECTORY }}/releases/${{ env.RELEASE_VERSION }}"
# Restore database
mysql -h ${{ secrets.DB_HOST }} -u ${{ secrets.DB_USER }} -p${{ secrets.DB_PASSWORD }} ${{ secrets.DB_DATABASE }} < database_backup.sql
echo "Rollback completed successfully." >> ${{ secrets.REMOTE_DIRECTORY }}/rollback.log
Deconstructing the Workflow
Let's break down what's happening in our ci-cd-pipeline.yml file step-by-step:
1. The Trigger (on: release: types: [created])
Our pipeline automatically starts running the moment you create a new release in your GitHub repository. This is super effective for production deployments, as it connects deployments directly to specific, versioned milestones.
2. The build-and-deploy Job
This is the main job that handles preparing your Drupal application and then getting it deployed to your remote server.
- Checkout code: First, it pulls your project's code from GitHub.
- Cache Composer dependencies: This helps speed things up on later runs by caching your vendor directory.
- Set up PHP and Composer: We configure the environment with the correct PHP version and Composer, making sure everything's ready for Drupal.
- Install Composer dependencies: This command installs all the necessary PHP packages your Drupal project relies on.
- Create production artifact / Download production artifact: These steps bundle your built application into an "artifact," which is basically a portable package that can be easily transferred.
- Upload artifact to remote server (SCP Action): Here, we use the appleboy/scp-action to securely copy your compiled Drupal application to a dedicated releases directory on your remote server. Each deployment gets its own folder (like releases/v1.0.0).
- Setup production deployment directory (SSH Action - Where Drush Does Its Thing!): This is where the magic happens. Using appleboy/ssh-action, we execute a series of commands directly on your remote server.
- Putting Your Site Offline Safely:
current/vendor/bin/drush config:set system.maintenance message "Our servers are getting a quick tune-up. Your patience is appreciated!" -y
current/vendor/bin/drush state:set system.maintenance_mode 1 --input-format=integer
current/vendor/bin/drush drush cr
- Before making any major changes, Drush puts your Drupal site into maintenance mode. It even displays a custom message for your users and clears the cache to ensure that message appears right away.
- Atomic Deployment (Smart Symlinks):
rm current
ln -s ${{ secrets.REMOTE_DIRECTORY }}/releases/${{ env.RELEASE_VERSION }} current
chmod +x current/vendor/bin/*
ln -s ${{ secrets.REMOTE_DIRECTORY }}/files ${{ secrets.REMOTE_DIRECTORY }}/releases/${{ env.RELEASE_VERSION }}/web/sites/default/files
ln -s ${{ secrets.REMOTE_DIRECTORY }}/settings.php ${{ secrets.REMOTE_DIRECTORY }}/releases/${{ env.RELEASE_VERSION }}/web/sites/default/settings/settings.php
rm ${{ secrets.REMOTE_DIRECTORY }}/public_html
ln -s ${{ secrets.REMOTE_DIRECTORY }}/releases/${{ env.RELEASE_VERSION }}/web ${{ secrets.REMOTE_DIRECTORY }}/public_html
- This is key for zero-downtime deployments. Instead of simply overwriting files, we upload the new version into a unique directory. Then, we update symbolic links (like current and public_html) to point to this new release. This way, your site is either running the old version or the new version completely, avoiding any broken states during the transition. Essential shared directories like files and settings.php are also linked to maintain data and configuration.
- Running Drush Database and Config Commands:
${{ env.RELEASE_VERSION }}/vendor/bin/drush updb -y
${{ env.RELEASE_VERSION }}/vendor/bin/drush cim -y
${{ env.RELEASE_DIRECTORY }}/vendor/bin/drush state:set system.maintenance_mode 0 --input-format=integer
${{ env.RELEASE_VERSION }}/vendor/bin/drush cr
- Once the new code is active, Drush handles crucial post-deployment tasks:
- drush updb -y: Runs any pending database updates.
- drush cim -y: Imports new configuration changes from your code into the database.
- drush state:set system.maintenance_mode 0: Takes the site out of maintenance mode.
- drush cr: Clears Drupal caches one final time.
- Keeping Things Tidy (Old Release Cleanup):
find ${{ secrets.REMOTE_DIRECTORY }}/releases -mindepth 1 -maxdepth 1 -type d -regex '.*/v[0-9]*\.[0-9]*\.[0-9]*' | grep -v "${{ env.RELEASE_VERSION }}" | sort -t '-' -k 1,1r | tail -n +2 | xargs rm -rf
- This command helps keep your server neat by automatically removing older release directories, typically retaining only the most recent versions (in this example, the last 5).
3. The backup_database Job
This job is a vital safety measure. It ensures that a fresh database backup is created before the deployment process makes any changes to your database.
- Create database backup (SSH Action): Uses mysqldump to create a backup of your Drupal database and stores it on the remote server. Peace of mind, right there!
4. The rollback Job
This job is a lifesaver! It's specifically configured to run only if the build-and-deploy job fails (if: failure()
).
- SSH and rollback (SSH Action):
- It identifies the previous stable release by checking your releases directory.
- Then, it simply switches the current symlink back to this previous, working version.
- The directory for the failed new release is then removed.
- Crucially, it restores the database from the database_backup.sql created by the backup_database job, ensuring both your code and data are rolled back to their prior state. Phew!
Key Takeaways and Best Practices
- GitHub Secrets are Essential: Notice how we use ${{ secrets.<NAME> }}. This is incredibly important for security. Never hardcode sensitive information (like SSH keys, usernames, passwords, or hostnames) directly in your workflow files. Store them securely as GitHub Secrets in your repository settings.
- Atomic Deployments for Smooth Transitions: The symlink strategy provides zero-downtime deployments and makes rollbacks straightforward.
- Always Backup Before Deploying: Make it a strict rule to back up your database before any significant deployment. You'll thank yourself if something unexpected happens.
- Maintenance Mode is Professional: Use Drush to gracefully put your site into maintenance mode during critical updates. It's a professional way to manage user experience during changes.
- Correct Drush Path: Double-check that your Drush executable is referenced correctly (e.g., current/vendor/bin/drush or ${{ env.RELEASE_VERSION }}/vendor/bin/drush) based on how Drush is installed in your project (typically in vendor/bin for Composer-based Drupal setups).
Ready to Streamline Your Drupal Workflow?
Bringing Drush and GitHub Actions together truly transforms your Drupal deployment process. What used to be a stressful, error-prone task becomes an automated, reliable, and honestly, pretty satisfying workflow. You can spend less time worrying about releases and more time building amazing things with Drupal.
For those who prefer a more hands-on approach, or if your hosting environment doesn't quite fit the GitHub Actions mold (like some shared hosting setups), you might be interested in where our automation journey began. We previously explored how to automate and simplify your Drupal workflow using Bash scripts, a method that provides powerful customization right from your command line. If a simpler, server-side scripting approach resonates more with your current needs, you can dive into that foundational guide
Why not give this workflow a try and adjust it to fit your own needs? If you have any questions or comments, drop them in the comments below. Happy deploying!
0 Comments
Login or Register to post comments.