Deploying and Building Laravel with GitHub Actions

- Introduction
- Step 1: Clone the Sample Project
- Step 2: Set Amezmo API Key and Site ID in GitHub Action Secrets
- Step 3: Add Environment Configuration in and Set the Public document root in Amezmo
- Step 4: Setup GitHub Action Workflow
- Step 5: Testing the Workflow
Introduction
In this guide, you'll learn how to deploy a Laravel Application with GitHub Actions on your Amezmo Instance using the Amezmo API without the need of installing Composer dependencies and building NPM assets at deployment time. The assets are built on GitHub, and then sent to Amezmo.
Prerequisites
- Laravel Basics
- GitHub Account
- Amezmo Account
Note: The source code is on GitHub at amezmo/github-actions-demo You may follow along or jump straight to the source code.
GitHub Actions
For those new to GitHub Actions, it is simply a way to trigger action or series of actions from events generated on a GitHub repository.
An example of this is building a workflow (series of actions) that automates the process of testing and building your code on a build server (Free GitHub-hosted virtual machines) before merging it. Doing this can reduce the risk of breaking stuff, which is especially important in a team project.
Think about it for a second, wouldn't it be cool if you can trigger a server somewhere that takes the changes you (or contributors) committed to your repository, run the test or series of actions, and get a report if it fails (and why it fails) or passes before being merged to the repo? It can save you a lot of time!
Typically, you'll need to test the changes thoroughly on your local machine, but when using GitHub Actions, you leverage their CI server that checks for new code commits in a repository all for free.
We can leverage this practice by using GitHub Actions for automatically deploying our code to production environment after ensuring that all of our specified actions have passed, the flow is something like below:
Forgive my diagram, but I believe you got the point: There are a couple of ways you can customize the workflow, you can either trigger actions from events when a pull request is submitted or when changes occur in a specific branch (you can even do the two). GitHub runs the actions and provides the results of each test in the pull request, this way, you can check why a workflow is failing.
Here is the scope of what we would be doing:
- Once a pull request is accepted into the master branch...
- it triggers our workflow which would contain 2 jobs [build and deploy] that would run sequentially, when one fails, it halts without moving to the next one.
- "build job" would be used to test our code, and we would also automatically bundle it into a zip file and release the artifacts, if that is successful, we deploy the project to production with the "deploy job"
Note: Artifacts are files produced during a workflow run, since we don't want to do composer install or npm install on the production stage, we use the one we've generated in our build, so, the artifact are the files generated when the build runs
Let's get into action:
Step 1: Clone the Sample Project
Note: If you already have an App you want to deploy, proceed to Step 2
I have provided the sample Laravel project on GitHub. The only dependency in this package is laravel/ui package, nothing more]. You may use the repository to follow along in this guide.
Clone it into your dev environment:
git clone https://github.com/amezmo/github-actions-demo
Import it into your GitHub Repo, and proceed to the next step.
Step 2: Set Amezmo API Key and Site ID in GitHub Action Secrets
Grab your API_KEY in your Amezmo app instance by clicking on your Avatar Icon -> Profile -> API Keys:

Copy the key to somewhere safe.
Get your SITE ID by navigating to the Overview tab, scroll all the way to Application details, copy the numeric ID right next to the ID label:

Now, go into your GitHub repo project Settings -> Secrets.
Click on "New repository secret" button add API_KEY
as the Name, and paste the Amezmo API Key you just copied as the value, for example:

Repeat the steps for SITE ID, add SITE_ID
as the Name, and paste the Amezmo SITE you just copied as the value.
Why are we doing this?
Short Answer: To hide sensitive information.
Long Answer: We would be exposing the API Key and SITE ID (the site we want to deploy the app into) in the workflow when deploying the app to production, keeping the key and site_id in the Action Secrets ensures it is encrypted and not plainly exposed to everyone that has access to the repo except those that have collaborator access.
Step 3: Add Environment Configuration in and Set the Public document root in Amezmo
Goto the configuration tab of your site instance in Amezmo, and add the following:
APP_NAME=SITE-NAME
APP_KEY=app_key
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=database_name
DB_USERNAME=database_user
DB_PASSWORD=database_pass
This would overwrites or create any .env
file in your application's root directory and is shared across deployments. In other words, instead of having to edit .env file for every deployment, you can just set it once here and it would be shared across deployment.
Feel free to add more environmental variable that pertains to your Laravel application.
Finalize this step by adding /public
under Public document root Which is located in the Nginx tab
Step 4: Setup GitHub Action Workflow
Workflows should be placed under .github/worflows directory, if you cloned the sample app repo, I already have a workflow named main.yml which is under .github/workflows, it contains the following:
name: Laravel Build and Deploy To Amezmo
on:
push:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest # Machine To Run On
services: # Service container Mysql
mysql: # Label used to access the service container
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: github_actions_demo
ports:
- 33306:3306
options: >- # Set health checks to wait until mysql database has started (it takes some seconds to start)
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v2
- name: Node.js Setup
uses: actions/setup-node@v2
with:
node-version: 16.x
- name: Cache node_modules directory
uses: actions/cache@v2
id: node_modules-cache
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}
- name: Install NPM packages
if: steps.node_modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Build frontend
run: npm run dev
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
- name: Get Composer Cache Directory 2
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v2
id: actions-cache
with:
path: '${{ steps.composer-cache.outputs.dir }}'
key: '${{ runner.os }}-composer-${{ hashFiles(''**/composer.lock'') }}'
restore-keys: |
${{ runner.os }}-composer-
- name: Cache PHP dependencies
uses: actions/cache@v2
id: vendor-cache
with:
path: vendor
key: '${{ runner.OS }}-build-${{ hashFiles(''**/composer.lock'') }}'
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.ci', '.env');" # If .env exist, we use that, if otherwise, copy .env.example to .env and use that instead
- name: Install Dependencies
if: steps.vendor-cache.outputs.cache-hit != 'true'
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Change Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Clear Config
run: php artisan config:clear
- name: Run Migration
run: php artisan migrate --force
- name: Create an Archive For Release
uses: montudor/action-zip@v0.1.0
with:
args: zip -X -r github-actions-demo.zip . -x ".git/*" "node_modules/*" "tests/*" ".github/*" composer.* package* phpunit.xml # We excluding git, node_modules, and others not needed in production
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: github-actions-demo
path: github-actions-demo.zip
- name: Upload Release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: github-actions-demo.zip
asset_name: github-actions-demo
tag: ${{ github.ref }}
overwrite: true
body: "Latest Release"
deploy:
needs: [build] # The below only runs if the build is succesful
runs-on: ubuntu-latest
steps:
- name: Download Latest ZIP Release
run: |
curl -s https://api.github.com/repos/amezmo/github-actions-demo/releases/latest | \
grep -E "browser_download_url" | grep -Eo 'https://[^\"]*' | xargs wget -O "latest.zip"
- name: Deploy The App To Amezmo
run: |
curl --request POST \
--url https://api.amezmo.com/api/sites/${{ secrets.SITE_ID }}/deployments \
--header 'authorization: Bearer ${{ secrets.API_KEY }}' \
--header 'content-type: multipart/form-data' \
--form environment=production \
--form archive=@latest.zip
Let's decipher the above:
Defining Event
name
is the name of the workflow, and is displayed on the repo actions page, if the name is omitted, GitHub sets it to the workflow file path relative to the root of the repository.
on
specifies how you the workflow should be triggered, in our case, we are triggering the workflow whenever a push occurs on the master
branch. A push can be when you commit a change to a branch.
name: Laravel Build and Deploy to Amezmo
on:
push:
branches: [master]
Build Job - Setting Up MySQL Service
A job can contain multiple steps and runs in an instance of the GitHub virtual environment, we started with the build
, in the runs-on, we set the machine we want the job to run, which is ubuntu in our case.
In other to make MySQL work, we add it to the services, we allowed empty password, specified database, added ports, and use the health check to see if mysql is working as expected.
jobs:
build:
runs-on: ubuntu-latest # Machine To Run On
services: # Service container Mysql
mysql: # Label used to access the service container
image: mysql:5.7
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: github_actions_demo
ports:
- 33306:3306
options: >- # Set health checks to wait until mysql database has started (it takes some seconds to start)
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
Checkout From the Repo
Still inside build
job, we defined steps with series of actions to execute, the following is used to check-out the repo, so your workflow can access it.
steps:
- uses: actions/checkout@v2
Setting Up Node.js
The below yml config setup Node.js with version 16.x (which should be the stable release), we cached the node_modules directory, this way, we can improve performance.
For example, we give the step an id node_modules-cache
, the key
under the with
is used for explictly storing and retriveing the cache, and whenever we want to install NPM packages, this steps.node_modules-cache.outputs.cache-hit != 'true'
check if it is a cache miss; that is the cache is not found, we install the npm ndepencies, however, if the cache is an hit; that is the cache is found, it would skip installing npm packages, and uses the one in the cache folder instead (which should increase performance)
- name: Node.js Setup
uses: actions/setup-node@v2
with:
node-version: 16.x
- name: Cache node_modules directory
uses: actions/cache@v2
id: node_modules-cache
with:
path: node_modules
key: ${{ runner.OS }}-build-${{ hashFiles('**/package.json') }}-${{ hashFiles('**/package-lock.json') }}
- name: Install NPM packages
if: steps.node_modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Build frontend
run: npm run dev
Setting Up PHP
This setup PHP with version 7.4 and we cached the vendor directory, this way, we can improve performance, the way the caching works is same as the NPM packages except this time, we are caching the vendor directory.
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
- name: Get Composer Cache Directory 2
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v2
id: actions-cache
with:
path: '${{ steps.composer-cache.outputs.dir }}'
key: '${{ runner.os }}-composer-${{ hashFiles(''**/composer.lock'') }}'
restore-keys: |
${{ runner.os }}-composer-
- name: Cache PHP dependencies
uses: actions/cache@v2
id: vendor-cache
with:
path: vendor
key: '${{ runner.OS }}-build-${{ hashFiles(''**/composer.lock'') }}'
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.ci', '.env');" # If .env exist, we use that, if otherwise, copy .env.example to .env and use that instead
- name: Install Dependencies
if: steps.vendor-cache.outputs.cache-hit != 'true'
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
In the above yml, I also copied the .env.ci to .env, you should create a file named .env.ci if you haven't already and add the following with your own env values:
APP_NAME=GitHub_Actions_Demo
APP_ENV=local
APP_KEY=
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=33306
DB_DATABASE=github_actions_demo
DB_USERNAME=root
Generating Key, and Running Migrations
Next up, we generate the APP_KEY, change the cache directory permission, cleared config, and lastly we run migration if you have anything to migrate:
- name: Generate key
run: php artisan key:generate
- name: Change Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Clear Config
run: php artisan config:clear
- name: Run Migration
run: php artisan migrate --force
Archiving The Artifact:
Moving on, we bundled the artifact (the files generated from the build) into a zip file named github-actions-demo.zip
since this would be shipped to our production environment, I excluded the folders that isn't useful in production: git, node_modules, tests, composer, etc
- name: Create an Archive For Release
uses: montudor/action-zip@v0.1.0
with:
args: zip -X -r github-actions-demo.zip . -x ".git/*" "node_modules/*" "tests/*" ".github/*" composer.* package* phpunit.xml # We excluding git, node_modules, and others not needed in production
Uploading Artifact and Releasing
This uploads the bundled artifact from the virtual environment, we then release it. Learn more about the actions/upload-artifact@v2 and svenstaro/upload-release-action@v2
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: github-actions-demo
path: github-actions-demo.zip
- name: Upload Release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: github-actions-demo.zip
asset_name: github-actions-demo
tag: ${{ github.ref }}
overwrite: true
body: "Latest Release"
That concludes the build
job, let's move on to the deploy
job
Deploy job - Deploying to Amezmo Using Amezmo API
The below yml config contains steps on how the deploy
job should run, we started by needing the [build] job, that is the job would only run if the build
job is successful.
We started by downloading the latest released file, we saved the output as latest.zip, to deploy the file to amezmo, we send a POST request to Amezmo API with the downloaded file. This ${{ secrets.SITE_ID }}
and this ${{ secrets.API_KEY }}'
is what we created in Step 2
deploy:
needs: [build] # The below only runs if the build is succesful
runs-on: ubuntu-latest
steps:
- name: Download Latest ZIP Release
run: |
curl -s https://api.github.com/repos/amezmo/github-actions-demo/releases/latest | \
grep -E "browser_download_url" | grep -Eo 'https://[^\"]*' | xargs wget -O "latest.zip"
- name: Deploy The App To Amezmo
run: |
curl --request POST \
--url https://api.amezmo.com/api/sites/${{ secrets.SITE_ID }}/deployments \
--header 'authorization: Bearer ${{ secrets.API_KEY }}' \
--header 'content-type: multipart/form-data' \
--form environment=production \
--form archive=@latest.zip
and that is it, whenever a push events occur in the master branch, it triggers the workflow.
Step 5: Testing the Workflow
To test this workflow, you can try making changes to your master branch, you can then check the Actions tab of your repo to see if any workflow is running.
If you are using the cloned sample project, I have intentionally added the following to the route/web.php file:
Route::get('/guest', function () {
return view('welcome');
});
Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Change the /guest
to only: /
and commit the changes, you can make the changes directly from GitHub U.I by clicking the pencil icon, once you are done with the changes, commit it and the workflow should get triggered
OR
To make it a bit more realistic, create a dev
branch (which would be the branch for developing features before being merged to the master branch):
git checkout -b dev
Open up route/web.php, change /guest
to /
save the file.
Add the file:
git add routes/web.php
Commit Changes
git commit -m "Update /guest to /"
Push changes to branch
git push origin dev
If git push is successful, compare and merge it to your master branch and that should trigger the workflow, here is how that steps looks:
Any questions?