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
就好,待會在抓下來改 -
Pull
git 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