Hey, need help with your Magento, WooCommerce or Laravel project? I have some availalility. Contact me.

Deploying Magento using Deployer and GitHub Actions

Magento 2, Continuous Deployment, GitHub Actions, Deployer, Hyvä

Magento 2 and deploying has quite some history. Before Magento 2.2 it was not possible to deploy Magento without any downtime. It needed a database connection to determine the deployment version. Luckily, things have changed for the good.

The Deployer Plus version by Juan Alonso was the default for a long time. It included everything that you needed to deploy Magento to your server. The downside of this version is that the development seems slow or dead. The last commit at the time of writing is 29 Dec 2021.

Luckily, another option is the included Magento 2 recipe from Deployer itself. The next steps are based on that.

Getting started

The first thing we need to do is install Deployer (composer require deployer/deployer) and create the deploy.php file. This can be done by running vendor/bin/dep init and selecting Magento 2 as option.

localhost

Ensure you have set the correct settings in your deploy.php: a repository and a host are required. This recipe also requires another host: localhost. This is required because the artifact approach is being used: The build is first done locally, and everything is packed into a file, which is then uploaded and unpacked. For this to work you need to add this line to your deploy.php:

localhost()->set('local', true);

Next to this, you also need to add at least one host to deploy to. You will usually have at least 2 (production and staging) or more. The examples in this article count on two different hosts: production and staging.

host('my-webshop.com')
    ->set('remote_user', 'deploy')
    ->set('deploy_path', '/var/www/html/my-webshop.com/')
    ->setHostname('123.456.789.123')
    ->setLabels(['stage' => 'production', 'branch' => 'main']);

host('staging.my-webshop.com')
    ->set('remote_user', 'deploy')
    ->set('deploy_path', '/var/www/html/staging.my-webshop.com/')
    ->setHostname('123.456.789.123')
    ->setLabels(['stage' => 'staging', 'branch' => 'develop']);

Theme options

There are also some optional settings for the setup:static-content:deploy step:

  • You can set the required languages:

    set('static_content_locales', 'en_US en_GB');

  • The number of jobs:
    set('static_content_jobs', '12');

  • Themes to compile. Not setting this will compile all themes:
    set('magento_themes', ['Magento/luma']);

  • Any other options you want to pass to the compile command (optional):
    set('static_deploy_options', '-f');

Hyvä support

Adding support for Hyvä is pretty easy. Just add these lines to your deploy.php file. I've added them before the deploy:unlock line:

// Deploy Hyva
task('hyva:deploy', function () {
    run('npm --prefix vendor/hyva-themes/magento2-default-theme/web/tailwind/ ci');
    run('npm --prefix vendor/hyva-themes/magento2-default-theme/web/tailwind/ run build');
});

before('magento:deploy:assets', 'hyva:deploy');

Exclude list

We will also need an exclude list file. The files on this list will not be included in the artifact. The artifact itself needs to be in this file, or else we will get an error because the file is altered during the compression. This makes sense, as we can't pack a file into itself.

For this, create a file in artifacts/exclude with this content:

artifacts/*

Preparing Magento

When trying to run these steps in GitHub Actions, Magento will try to connect to the database to which we don't have access. To prevent this, Magento has the option to dump the configuration settings around the stores and themes in your app/etc/config.php file. To do that, run this command:

bin/magento app:config:dump scopes themes

Now make sure that you commit these changes before continuing.

Preparing GitHub Actions

GitHub Actions needs access to your server. For this, we need access to a private/public key pair. The private key needs to be in GitHub Actions; the public key needs to be placed on your server in ~/.ssh/authorized_keys.

Open your repository and navigate to Settings -> Secrets and Variables -> Actions. Click "Add new repository secret".

Name:
SSH_PRIVATE_KEY

Value:
-- Insert your private key --

A key pair can be generated locally, via a website, or, my preference, through 1Password.

deploy.yml

This is the deploy.yml I'm using. It is placed in .github/workflows/deploy.yml:

name: Deploy

on:
  push:
    branches:
      - main
      - develop

jobs:
  deploy:
    concurrency: deploy-${{ github.head_ref || github.ref_name }}

    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Cache vendor
        uses: actions/cache@v3
        with:
          path: |
            vendor
            !vendor/magento/magento2-base/
            ~/.composer/cache
          key: vendor-${{ hashFiles('**/composer.lock') }}

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'

      - name: Setup Node
        uses: actions/setup-node@v2
        with:
          node-version: '16'

      - name: Install Composer dependencies
        run: composer install --no-dev --prefer-dist --optimize-autoloader

      - name: Build artifact
        run: vendor/bin/dep artifact:build localhost

      - name: Deploy artifact
        uses: deployphp/action@v1
        with:
          private-key: ${{ secrets.SSH_PRIVATE_KEY }}
          dep: artifact:deploy branch=${{ github.head_ref || github.ref_name }}

      - name: Upload artifact
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: Artifact - ${{ github.head_ref || github.ref_name }}
          path: |
            artifacts/artifact.tar.gz

Let's go through it.

On push

This code:

on:
  push:
    branches:
      - main
      - develop

Makes sure that Magento is only build & deployed when a push is done on the main and develop branches. Any other branches will be ignored.

Concurrency

jobs:
  deploy:
    concurrency: deploy-${{ github.head_ref || github.ref_name }}

This line makes sure that there is only 1 deployment per branch. This makes sure you don't get conflicts where the first push locks the deployment and the next push fails because the deployment is locked.

Steps

uses: actions/checkout@v2

This makes sure the code is pulled from GitHub into the runner.

name: Cache vendor

Composer is pretty quick nowadays, but it can be even quicker by caching the files. The magento/magento2-base is not being cached. The reason for this is that a composer install action will trigger some scripts that copy bin/magento and other required files.

name: Setup PHP

We need PHP, so make sure it is available.

name: Setup Node

This is mainly for Hyvä, but make sure that Node and NPM are available.

name: Install Composer dependencies

This speaks for itself.

name: Build artifact

This is where the first magic happens: All steps that can be done locally will be done. setup:di:compile, setup:static-content:deploy, etc. The last action in this step will pack everything into a file for the next step.

name: Deploy artifact

The file generated in the previous step will be uploaded and unpacked. Also, some of the steps that can only be done on the server will be executed here. Things like creating symlinks to app/etc/env.php and running setup:upgrade if required.

name: Upload artifact

This step is optional: The generated artifact file will be uploaded and available withing GitHub actions when the build is complete. This way, you can always inspect the generated code afterward.

MageDispatch.com (ad)

Hey, I'm running a developer focussed newsletter called MageDispatch.com, which is for the community and by the community. Here you can share links that you think that the community should know about. If you like this kind of technical content you will like this newsletter too.

Want to respond?