Skip to main content
info@drupalodyssey.com
Wednesday, July 23, 2025
Contact

Main navigation

  • Home
  • Blog
    • all
    • Development
    • Community
    • Management
    Contractor happy with blueprints.
    Streamlining Drupal Deployments Using Drush and GitHub Actions CI/CD
    Jul 22, 2025
    Reduce, Reuse, Recycle
    Streamlined Content: Effortless PDF Display & Management in Drupal
    Jul 21, 2025
    Aluminum Cans Passing Through the Assembly Line by cottonbro studio on Pexels
    Automate and Simplify Your Drupal Workflow with Bash Scripts for Shared Hosting
    Jul 19, 2024
    Binoculars resting on newspapers.
    Evaluating Search and Replace Scanner: The Ultimate Tool for Drupal Bulk Content Edits?
    Jun 29, 2024
    People looking at a computer screen
    S3 File System Module Not Working with Media Entity Download Module? Here's the Fix
    Jun 18, 2024
    Mechanic hands working on an engine.
    Setting Up the Etsy OAuth2 Client For Use With The Etsy Shop Integration Module
    May 10, 2024
    Fashion designer sketching new garments.
    Crafting Your Online Store: Drupal's Role in Your Etsy Success
    May 09, 2024
    Socket toolbox
    Beginner's Guide: Getting Started With Drush for Efficient Drupal Development
    May 08, 2024
    Stargazing over mountians.
    Drupal-Powered Stargazing: A Module for NASA's Astronomy Picture of the Day
    Sep 15, 2023
    Computer screen with code.
    Learn How To Script Drupal Installations Using Drush
    Dec 08, 2014
    Scuba diver with Drupal mask.
    Scuba: Drupal Style
    Oct 16, 2014
    Woman frustrated with laptop.
    5 Reasons Your CMS Sucks
    Jul 24, 2013
    Two young men having a discussion in front of a computer.
    Deployment Module XSRF Patch Committed
    Jul 05, 2013
    Two young men having a discussion in front of a computer.
    Deployment Module XSRF Patch Committed
    Jul 05, 2013
    Application settings.
    Using PHP To Disable Internet Explorer Compatibility Mode
    Jun 04, 2013
  • Resources
  • About
  • SPACER
  • SPACER
  • SPACER
  • SPACER
  • SPACER
Search
Development

Streamlining Drupal Deployments Using Drush and GitHub Actions CI/CD

July 22, 2025

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
here
.

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!

Author

Ron Ferguson

Previous Blog

Next Blog

0 Comments

Login or Register to post comments.

Categories

Categories

  • Development
    (10)
  • Community
    (9)
  • Management
    (5)

Trending Blog

Trending Blog

Contractor happy with blueprints.
Streamlining Drupal Deployments Using Drush and GitHub Actions CI/CD
22 Jul, 2025
Woman frustrated with laptop.
5 Reasons Your CMS Sucks
24 Jul, 2013
Mechanic hands working on an engine.
Setting Up the Etsy OAuth2 Client For Use With The Etsy Shop Integration Module
10 May, 2024
Stargazing over mountians.
Drupal-Powered Stargazing: A Module for NASA's Astronomy Picture of the Day
15 Sep, 2023
People looking at a computer screen
S3 File System Module Not Working with Media Entity Download Module? Here's the Fix
18 Jun, 2024

Tags

Tags

  • Drupal 10
  • Drupal 9
  • Drupal 8
  • Drupal 11
  • Drupal 7
  • Drush

Ad - Sidebar (300 x 250 AD)

Ad - Sidebar (300 x 600 AD)

Newsletter

Subscribe my Newsletter for new blog & tips Let’s stay updated!

Categories

  • Development
  • Community
  • Management

Useful Links

  • About
  • Contact
  • Privacy Policy
  • Terms & Conditions
  • Disclaimer
  • Cookies

Must Read

Aluminum Cans Passing Through the Assembly Line by cottonbro studio on Pexels
Automate and Simplify Your Drupal Workflow with Bash Scripts for Shared Hosting
19 Jul, 2024
Binoculars resting on newspapers.
Evaluating Search and Replace Scanner: The Ultimate Tool for Drupal Bulk Content Edits?
29 Jun, 2024
Mechanic hands working on an engine.
Setting Up the Etsy OAuth2 Client For Use With The Etsy Shop Integration Module
10 May, 2024
Socket toolbox
Beginner's Guide: Getting Started With Drush for Efficient Drupal Development
08 May, 2024

© 2024 All Rights Reserved.

Proud supporter of active military, veterans and first responders.