innocenzi/deployer-recipe-forge

Seamless zero-downtime deployment on Forge with Deployer

0.3.3 2023-09-26 16:27 UTC

This package is auto-updated.

Last update: 2024-04-15 12:33:52 UTC


README

release version

Seamless zero-downtime deployment on Forge with Deployer

composer require --dev innocenzi/deployer-recipe-forge

 

About

deployer-recipe-forge is a recipe for Deployer that helps implementing zero-downtime on Laravel Forge.

It uses Forge's API to fetch a site's credentials, such as its IP address and the remote username, so you don't have to hardcode them in your deploy.php. It automatically finds the right Forge site using your repository's name, so the only configuration to do happens on Forge.

Additionally, it's able to send notifications on Slack with either Deployer's integration, which requires that you setup a webhook, or by pinging Forge's deployment URL.

 

Installation

Install the package as a development dependency:

composer require --dev innocenzi/deployer-recipe-forge

 

Then, create a deploy.php file at the root of your project. Import the autoloader, and call \Deployer\Forge::make()->configure();. You may then customize your deployment script as usual with Deployer.

namespace Deployer;

// This is required
require __DIR__ . '/vendor/autoload.php';
\Deployer\Forge::make()->configure();

// This is your custom deployment script
task('deploy:build', function () {
	  // build assets here
    runLocally('bun i');
    runLocally('bun run build');
});

 

Usage

Setting up the repository

This recipe is designed to be used within a GitHub workflow, using the action provided by Deployer.

Setting it up requires at least two repository secrets:

  • Your server's private SSH key, which will be used by deployphp/action to SSH
  • Your Forge API token, which will be used by the recipe to fetch your site and server's credentials

These repository secrets must then be forwarded to the action as environment variables, or, for the SSH key, as the private-key argument:

- uses: deployphp/action@v1
  with:
    private-key: ${{ secrets.PRIVATE_SSH_KEY }}
    dep: deploy
  env:
    FORGE_API_KEY: ${{ secrets.FORGE_API_KEY }}
    REPOSITORY_BRANCH: ${{ github.ref_name }}
    REPOSITORY_NAME: ${{ github.repository }}

 

Warning

Note that the REPOSITORY_BRANCH and REPOSITORY_NAME variables are required for the recipe to work, since they are used to match your site and branch to your site Forge.

Note

For convenience, if you are using a paid GitHub plan, you may setup your Forge API token and Slack webhook as organization-wide secrets.

 

Setting up Forge

Creating a new site

The following steps explain how to setup a new site on Forge to work with this recipe on GitHub Actions.

  1. Create an isolated site.

    Currently, this recipe only works with isolated Forge sites. You must then create an isolated site.

  2. Install the repository using the GitHub integration.

    You may uncheck "install composer dependencies" to make the installation faster, as the site directory will be erased anyway.
    If you haven't associated the server's deploy key with your GitHub repository, make sure that you do or that you create a deploy key for this site.

  3. If necessary, add the server's public key (~/.ssh/id_rsa.pub) to the repository's deploy keys.

    This will allow Deployer to clone the repository from its SSH session on the server.

  4. SSH into the server to copy its public key (~/.ssh/id_rsa.pub) and add it to the server's authorized keys on Forge.

    This is the first step to allow the Deployer action to SSH into the server.
    The key should be associated to the correct isolated site username.

  5. Finally, copy the server's private key (~/.ssh/id_rsa) and add it to the repository's secrets.

    This is the last step to allow the Deployer action to SSH into the server.
    We suggest naming the secret after the target environment. For instance, if you are creating a staging site, it may be named STAGING_SSH_KEY.

  6. You may then trigger the deployment workflow.

 

Adding to an existing site

First, make sure the website is isolated. It must be located at /home/<isolated-username>/<site-name> on the server. Otherwise, you must create a new isolated site.

You must first backup your .env file and storage directory, since Deployer will delete the site's directory to replace it with a symlink.

After backing them up, you may follow steps 3 to 6. Once the deployment is complete, you may restore your .env and storage in /home/<isolated-username>/deployer/<site-name>/shared.

 

Warning

Proceed with caution. Make sure to put the site on maintenance and think of the potential side-effects that may happen during this migration.

 

Slack notifications

Using Deployer

This recipe supports sending Slack notifications using a webhook. To create it, you may follow Slack's documentation on the topic. You must then add the webhook as a repository or organization secret, and forward it to the action as an environment variable.

- uses: deployphp/action@v1
  with:
    private-key: ${{ secrets.PRIVATE_SSH_KEY }}
    dep: deploy
  env:
    # ...
    # Add these two
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    RUNNER_ID: ${{ github.run_id }}

Note

Note that RUNNER_ID is required, because the Slack notifications link to the associated GitHub workflow.

 

Using Forge

Alternatively, you may set up Slack notifications on Forge, and trigger a deployment on Forge. This way, you also get to use Forge's deployment history.

To trigger the deployment on Forge, simply call triggerDeploymentsOnForge:

\Deployer\Forge::make()
	->triggerDeploymentsOnForge()
	->configure();

Warning

When using this strategy, make sure to empty the default deployment script configured by Forge.

 

Multiple environments

This recipe supports deploying to multiple environments, such as staging or production, without any specific configuration, other than adding the different private SSH keys to your repository secrets.

It works by associating the repository branch to the one defined on the Forge site. For instance, you may have a main branch associated to example.com, and a develop branch associated to staging.example.com.

 

For two environments

The private SSH key must be provided to the private-key argument, for instance using a conditional, as follows:

- uses: deployphp/action@v1
  with:
    private-key: ${{ github.ref_name == 'develop' && secrets.STAGING_SSH_KEY || secrets.PRODUCTION_SSH_KEY }}
    dep: deploy
  env:
    # ...

 

For more environments

If you have more than two environments, you may simply duplicate the actions and use an if statement to ensure they run for the correct branches:

steps:
  - name: Deploy (staging)
    if: github.ref_name == 'develop'
    uses: deployphp/action@v1
    with:
      private-key: ${{ secrets.STAGING_SSH_KEY }}
      dep: deploy
    # ...

  - name: Deploy (production)
    if: github.ref_name == 'main'
    uses: deployphp/action@v1
    with:
      private-key: ${{ secrets.PRODUCTION_SSH_KEY }}
      dep: deploy
    # ...

 

Building on CI or on server

By default, the recipe will build assets on CI, and upload them on the server after. To build on the server, call buildOnServer:

\Deployer\Forge::make()
    ->buildOnServer()
    ->configure();

Note that, by default, no deploy:build task is provided, and you should write your own. For instance, you may build assets using Bun:

\Deployer\Forge::make()->configure();

task('deploy:build', function () {
    runLocally('bun i');
    runLocally('bun run build');
});

Obviously, make sure Bun is installed on CI before using that example.

Example workflow

This workflow deploys on production or staging, depending on the branch that pushed, after running the test.yml and style.yml workflows. It will skip deployments if the commit body contains [skip deploy], and will notify about deployments on Slack.

Additionally, you may dispatch the workflow manually.

name: Deploy

on:
  workflow_dispatch:
  push:
    branches: [main, develop]

concurrency:
  group: ${{ github.ref }}

jobs:
  test:
    uses: ./.github/workflows/test.yml

  style:
    uses: ./.github/workflows/style.yml

  deploy:
    needs: [test, style]
    if: ${{ !contains(github.event.head_commit.message, '[skip deploy]') }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - uses: shivammathur/setup-php@v2
        with:
          php-version: 8.2

      - uses: oven-sh/setup-bun@v1
        with:
          bun-version: latest

      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist

      - uses: deployphp/action@v1
        with:
          private-key: ${{ github.ref_name == 'develop' && secrets.STAGING_SSH_KEY || secrets.PRODUCTION_SSH_KEY }}
          dep: deploy
        env:
          FORGE_API_KEY: ${{ secrets.FORGE_API_KEY }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          REPOSITORY_BRANCH: ${{ github.ref_name }}
          REPOSITORY_NAME: ${{ github.repository }}
          RUNNER_ID: ${{ github.run_id }}

 

Using dep locally

Since the recipe depends on multiple environment variables, you cannot easily call vendor/bin/dep locally.

You may work around that limitation by adding FORGE_API_KEY to your .env, as well as REPOSITORY_NAME and REPOSITORY_BRANCH:

FORGE_API_KEY="abc...123"
REPOSITORY_NAME="yourorg/repo"
REPOSITORY_BRANCH="develop"

Then, you may add the following script to composer.json, and run composer dep instead of vendor/bin/dep:

"dep": "set -o allexport && source ./.env && set +o allexport && vendor/bin/dep"

Warning

Using dep locally will create a .deployer_cache file, which you should add to .gitignore.

Note

If you get denied SSH access, make sure to add your public key on your server and associate it to your site's user.

 

Q&A

Why not Envoyer?

Envoyer's integration with Forge removes the convenience of being able to manage everything within Forge directly, instead forcing us to juggle between two different user interfaces. Our developer experience is better with Deployer and this recipe.

Why are issues closed?

This recipe is mostly used internally at Jetfly, and we are not willing to do support for it nor add many features. Pull requests for fixes and small additions may be welcome, though.