Github Actions CI/CD With Laravel
tags: Laravel Github Actions
category: DevOps
description: Github Actions CI/CD With Laravel
created_at: 2022/08/16 12:00:00

前言
這篇會跟著上一篇 - Laravel 連接 MSSQL往下走,所以會假設以下:
- 應用程式:
Laravel Sail - 資料庫:
MSSQL - 作業系統:
Windows Server
雖然說環境是這樣,不過反正在依樣畫葫蘆改成自己的環境就好。
建立 Repo
既然是 Github Actions 跑在 Github 上,當然就要先建立一個 Repository,然後可以先把 Code 先推上去,用介面產生設定檔的樣板在 Pull 下來。
-
先點到
Actions的頁籤,然後找到自己使用的環境(也可以搜尋)
-
產生樣板之後直接
Commit就好,待會在抓下來改
-
Pullgit pull origin main
預設設定檔
因為設定檔有點長,所以直接貼 Code 加上註解來說明
# 這個 workflow 的名稱,會顯示在 Actions 左邊的 Tab
name: Laravel
# 觸發條件: 在什麼情況會觸發下面的工作(jobs)
on:
# 當 push
push:
# 只運作在 main 分支
branches: [ "main" ]
# 依樣畫葫蘆
pull_request:
branches: [ "main" ]
# 定義工作,可以有多個,這邊預設目前一個
jobs:
# 跑測試的工作
laravel-tests:
# 跑在 ubuntu-latest 作業系統環境
runs-on: ubuntu-latest
# 一個 job 會有多個步驟
steps:
# 使用別人寫好的 action,像這個是設定安裝php環境
- uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
# 也可以傳值過去
with:
# 跟他說我要 8.0 的 PHP
php-version: '8.0'
# 使用官方提供的 action,把 code 抓下來
- uses: actions/checkout@v3
# 幫每個 step 取名字,一樣不能重複
- name: Copy .env
# 這個 step 就是 run 一個指令,這邊是執行 php 程式
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install Dependencies
# 安裝套件
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
# 生成 key
run: php artisan key:generate
- name: Directory Permissions
# 調整權限
run: chmod -R 777 storage bootstrap/cache
- name: Create Database
# 這邊是執行多個指令的寫法,但之後會刪掉,因為我們不是用 sqlite
run: |
mkdir -p database
touch database/database.sqlite
- name: Execute tests (Unit and Feature tests) via PHPUnit
# 設定環境變數
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
# 跑測試囉
run: vendor/bin/phpunit
上面是他預設產出來的樣板,接下來要改成我要的。
改設定檔之前
如果只是留著最基本的測試根本不會知道上去的 MSSQL 是不是好的,所以來加一個基本的測試是測試 migrate 之後 seed 完看看資料長度是否正確。
修改 /tests/Feature/ExampleTest.php
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
/**
* A basic test example.
*
* @return void
*/
public function test_init_10_data_to_users_table()
{
$this->seed();
$users = \App\Models\User::get();
$this->assertEquals($users->count(), 10);
}
}
然後也可以先在本地戳戳看,跑下面這段,應該是會通過。
sail artisan test
再來就可以開始改 Github Actions 的設定檔了
在真的動手改設定檔之前
在上面預設的設定檔說明會看到環境變數的部分,可以先想像一下,我們的環境變數都在那個設定檔中,那代表會上 git,那麼就是大家都會看光光,那我們可能有些密碼之類的不能給人看,所以要放在一個特定的位置,在 Github Actions 是放在設定中。
-
先點到
Settings的頁籤,然後在左邊找到Secrets下面有一個Actions,而右邊就會有一顆新增的按鈕。
-
新增完成之後會看到他只能修改或刪除,也就是你忘記了只能重新建,也看不到原本設定的東西。

設定完成之後,要在設定檔中帶入也很簡單,只要輸入像是下面這樣
${{ secrets.DB_PASSWORD }}
這個只能帶入進去,沒辦法被輸出出來看,Github 會幫你把他變成固定長度的 * 字號做隱藏,意思是說他可能固定5個長度,就算你存的資料長度不是5,他還是顯示5個*字號,比較安全。
真的開始改設定檔
首先我這邊環境希望是 php7.3,所以最先動的肯定是 php-version 的部分
再來因為他的 .env 是由 .env.example 產生的,所以可以先把範例改的差不多,這邊假設只把密碼抽出來之後帶入,所以密碼為空。
備註: 這邊也可以自己定義怎麼產生 .env,例如你可以把整包 .env 用到的直接塞進 secrets
假設我把資料庫相關的預設值改成這樣:
DB_CONNECTION=sqlsrv
DB_HOST=mssql
DB_PORT=1433
DB_DATABASE=laravel
DB_USERNAME=sa
DB_PASSWORD=
DB_TRUST_SERVER_CERTIFICATE=true
雖然改的差不多了,不過第一次改設定檔還是不要改太多,先測試至少在上面跑基本的 CI 是可以的,確保 Laravel Sail 的環境至少搭得起來,資料庫能不能動先不管。
也就是先弄成這樣
name: Laravel
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
laravel-tests:
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
with:
php-version: '7.3'
- uses: actions/checkout@v3
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Sail Build
run: ./vendor/bin/sail build --no-cache
- name: Sail Up -d
run: ./vendor/bin/sail up -d
- name: Execute tests (Unit and Feature tests) via PHPUnit
run: ./vendor/bin/sail artisan test
這時我先只推送這個設定檔上去,會看到他跑很久之後在 up 的時候出問題,出現下面的訊息
Error response from daemon: failed to create shim: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "./entrypoint.sh": permission denied: unknown
這時候是因為權限不足的關係,因為在 local 可能已經有了 x 執行的權限,但上去測試機可能沒有,所以要幫他加上一個 step
# ...
- name: MSSQL shell script Permissions
run: |
chmod +x ./docker/mssql/entrypoint.sh
chmod +x ./docker/mssql/configure-db.sh
- name: Sail Build
run: ./vendor/bin/sail build --no-cache
# ...
必須要放在 Build 之前,因為 build 就會去跑那個 Dockerfile,雖然執行是在 up 但是包裝的時候是在 Build ,如果打包的時候沒有權限,包完才改權限那 up 也吃不到。
這時候應該就可以看到通過了,再來就可以加上資料庫相關的設定了。
資料庫相關的設定
基本上就是希望在 up 之後幫我跑 migrate 與 seed,所以加入下面這個 step
- name: Migrate and Seed
run: ./vendor/bin/sail artisan migrate --seed
然後可以加上 env 的設定去代換 DB_PASSWORD 的值:
# ...
runs-on: ubuntu-latest
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
# ...
這時候可以把上面改過的 .env.example 跟 ExampleTest 都推上去了,因為要吃新的設定,而不是舊的 MySQL 設定,順便也套上新的測試程式。
當你推上去之後跑 CI 應該會噴錯
SQLSTATE[HYT00]: [Microsoft][ODBC Driver 18 for SQL Server]Login timeout expired (SQL: select * from sys.sysobjects where id = object_id(migrations) and xtype in ('U', 'V'))
這邊要先提一個小坑
Env
總之這邊要先紀錄一個小坑,就是上面有提到 env,他是幫你直接帶入讓你的應用程式去用,但是因為我們這邊使用 Laravel Sail 他多包了一層,所以他吃不到這個環境變數,為了測試這個問題,可以建立一個測試程式。
php artisan make:test PrintEnvTest
之後把裡面填入下面這段把環境變數印出來。
public function test_print_env()
{
dump($_ENV);
dump('DB_PASSWORD: ', env('DB_PASSWORD'));
$this->assertTrue(true);
}
然後在 Github Actions 的設定檔中加入兩個 step,分別測試 php 與 sail 環境的測試,也可以另外加 cat .env 把他印出來看看。
# ...
- name: Sail Up -d
run: ./vendor/bin/sail up -d
# 補上下面兩個
- name: Debug print env with sail
run: ./vendor/bin/sail artisan test --filter test_print_env
- name: Debug print env with php
run: php artisan test --filter test_print_env
# ...
這時他跑完會看到下面這樣
Debug print env with sail
"DB_PASSWORD: "
""
Debug print env with php
"DB_PASSWORD: "
"***"
如果自己有另外把 .env 印出來,也會發現他不是真的去改 .env 這個檔案的內容。
所以這樣會導致 MSSQL 的密碼沒吃到,那他建立的時候沒密碼當然後掛了,所以才會連線逾時。
所以為了解決這個問題,就是要在 CI 過程中動態建立或修改 .env 的檔案,讓他吃到密碼的設定。
# ...
- name: Generate key
run: php artisan key:generate
# 補上下面這段
- name: Set DB_PASSWORD
run: sed -i 's/DB_PASSWORD=/DB_PASSWORD=${{ secrets.DB_PASSWORD }}/g' .env
# ...
基本上上面這段就是把 DB_PASSWORD= 這段取代成帶入密碼後的樣子。
所以在我們這個環境中是不需要設定 env 的,也就是可以移除下面這段
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
又出錯了?!
這時在 migrate 那邊會再碰到一個錯誤
SQLSTATE[42000]: [Microsoft][ODBC Driver 18 for SQL Server][SQL Server]Cannot open database "laravel" requested by the login. The login failed. (SQL: select * from sys.sysobjects where id = object_id(migrations) and xtype in ('U', 'V'))
他說無法開啟資料庫,資料庫不在,但是在 Local 正常啊(?),不相信再自己跑一次(?)
先關掉
sail down
再打開
sail up -d
再 migrate
sail artisan migrate --seed
如果噴掉的話可能要檢查你的 .env 在 up 之前是否設定正確;但是基本上應該是正常 migrate 成功的。
而到 CI 上面卻失敗,原因是因為當初在設定 /docker/mssql/entrypoint.sh 的時候,先看一下 code
#!/bin/bash
/usr/config/configure-db.sh & # 跑在背景
/opt/mssql/bin/sqlservr # 跑 sqlserver 在背景常駐執行
因為手動 migrate 的時候他可能已經執行完 configure-db.sh 這個建立資料庫的腳本了,但在 CI 上面他執行很快,所以 configure-db.sh 還沒跑完,導致資料庫還沒被建立。
所以最簡單的方式就是在 CI 上面保證 configure-db.sh 被執行,所以可以多塞一個 step,而因為 configure-db.sh 這個腳本執行多次並不會有副作用,所以可以直接用,至少可以保證資料庫被建立完成。
所以可以在 migrate 之前加入下面這段
- name: Create database
run: docker exec `docker ps -f "ancestor=mssql-custom" -q` /usr/config/configure-db.sh
因為在 docker-compose 中有定義他建立的 image 名稱為 mssql-custom,所以直接這樣子讓他執行指令。
然後改好之後推上去再跑一次 CI 應該就會通過了。
Build 太慢
不只 Build 太慢,而且還可能碰到相關的套件剛好載不下來的問題(雖然可能不常見,但我就碰到了),所以可以趁還能載的時候先丟上 DockerHub,這樣之後只是拉 image 下來,不是在從網路上到處載套件下來,速度就會快很多。
CD 的部分
上面那一大坨都在搞 CI ,現在要開始弄 CD 了,這邊會以 Windows Server 當範例,其他環境也大同小異,反正在轉換就好,反正主要都是把東西丟上去(?),再來要怎麼搞都行
為了把東西丟到 Windows Server 上面,我打算使用 self-hosted。
self-hosted
Github Actions 有提供用你自己的機器來跑 CI/CD ,因為如果用 Github 提供的會有限制的分鐘數(或是花錢),如果用自己的機器則就不限,而用法也很簡單,一樣先到設定裡面:
-
找到設定頁面

-
點擊新增

基本上就選好自己的作業系統,然後跟著上面精簡的文件做操作就可以了,設定好就會看到他有被列出來。
之後如果你要用的話就要把 runs-on 改成 self-hosted,然後這邊順便來一個 Hello world 確定他有正常運作,所以建立一個新的 job
laravel-tests:
runs-on: ubuntu-latest
# ...略
laravel-deploy:
runs-on: self-hosted
needs: laravel-tests # 需等 laravel-tests 正常跑完才會跑
steps:
- name: Hello world
run: echo Hello world
如果想快速測試的話也可以先把 needs 拿掉,不等 test 跑完。
這時應該會看到他順利的執行成功,再來就是要想怎麼把東西丟到 self-hosted 這台機器上,可以使用 Github 寫好的一個 Action ,把檔案暫時存上去,在下載下來。
這這次用的專案畢竟不是編譯的專案,檔案有點多,還是先壓縮一下在傳送比較好,所以需要加入兩個 step
laravel-tests:
runs-on: ubuntu-latest
steps:
# ...略
- name: Zip project
run: zip -r ${{ github.event.repository.name }}.zip . -x ".git/*" ".github/*" "phpcs.xml" "composer.json" "composer.lock" ".gitignore"
- name: Upload artifact for development job
uses: actions/upload-artifact@v3
with:
name: laravel-app
path: ${{ github.workspace }}/${{ github.event.repository.name }}.zip
這邊看到的 github.xxx 相關的值也是 github 官方定義的環境變數。
在 zip 的部分 -x 可以指定過濾的檔案,可以把不用上 Production 環境的檔案過濾掉。
而這邊這個 actions/upload-artifact@v3 就是官方寫好的一個 action,可以把檔案上傳到一個暫存的地方,之後下載會以下方定義的 name 做對應。
然後這時候 laravel-deploy 的設定就變成了這樣
laravel-deploy:
runs-on: self-hosted
needs: laravel-tests
steps:
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: laravel-app
- name: Extrace project files
run: 7z x ${{ github.workspace }}/${{ github.event.repository.name }}.zip
- name: Remove zip
run: Remove-Item ${{ github.workspace }}/${{ github.event.repository.name }}.zip
基本上就是這個步驟
- 下載
- 解壓縮
- 刪除壓縮檔
注意: 這裡假設解壓縮使用 7z,所以你的機器上應該要有 7z 這個指令,不然就要代換成其他解壓縮的方式。
然後推上去再跑一次。
跑完之後你的 server 上就有整包專案的程式了,就可以在根據自己的需求調整怎麼樣 Deploy。
最後
完整的設定檔:
name: Laravel
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
laravel-tests:
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
with:
php-version: '7.3'
- uses: actions/checkout@v3
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install Dependencies
run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Set DB_PASSWORD
run: sed -i 's/DB_PASSWORD=/DB_PASSWORD=${{ secrets.DB_PASSWORD }}/g' .env
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: MSSQL shell script Permissions
run: |
chmod +x ./docker/mssql/entrypoint.sh
chmod +x ./docker/mssql/configure-db.sh
- name: Sail Build
run: ./vendor/bin/sail build --no-cache
- name: Sail Up -d
run: ./vendor/bin/sail up -d
- name: Create database
run: docker exec `docker ps -f "ancestor=mssql-custom" -q` /usr/config/configure-db.sh
- name: Migrate and Seed
run: ./vendor/bin/sail artisan migrate --seed
- name: Execute tests (Unit and Feature tests) via PHPUnit
run: ./vendor/bin/sail artisan test
- name: Zip project
run: zip -r ${{ github.event.repository.name }}.zip . -x ".git/*" ".github/*" "phpcs.xml" "composer.json" "composer.lock" ".gitignore"
- name: Upload artifact for development job
uses: actions/upload-artifact@v3
with:
name: laravel-app
path: ${{ github.workspace }}/${{ github.event.repository.name }}.zip
laravel-deploy:
runs-on: self-hosted
needs: laravel-tests
steps:
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: laravel-app
- name: Extrace project files
run: 7z x ${{ github.workspace }}/${{ github.event.repository.name }}.zip
- name: Remove zip
run: Remove-Item ${{ github.workspace }}/${{ github.event.repository.name }}.zip