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

cover image


前言

這篇會跟著上一篇 - Laravel 連接 MSSQL往下走,所以會假設以下:

  • 應用程式: Laravel Sail
  • 資料庫: MSSQL
  • 作業系統: Windows Server

雖然說環境是這樣,不過反正在依樣畫葫蘆改成自己的環境就好。


建立 Repo

既然是 Github Actions 跑在 Github 上,當然就要先建立一個 Repository,然後可以先把 Code 先推上去,用介面產生設定檔的樣板在 Pull 下來。

  1. 先點到 Actions 的頁籤,然後找到自己使用的環境(也可以搜尋) github-actions-laravel-tag

  2. 產生樣板之後直接 Commit 就好,待會在抓下來改 github-actions-laravel-config

  3. 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 是放在設定中。

  1. 先點到 Settings 的頁籤,然後在左邊找到 Secrets 下面有一個 Actions,而右邊就會有一顆新增的按鈕。 github-actions-secrets-setting

  2. 新增完成之後會看到他只能修改或刪除,也就是你忘記了只能重新建,也看不到原本設定的東西。 github-actions-secrets-list

設定完成之後,要在設定檔中帶入也很簡單,只要輸入像是下面這樣

${{ 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 之後幫我跑 migrateseed,所以加入下面這個 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.exampleExampleTest 都推上去了,因為要吃新的設定,而不是舊的 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,分別測試 phpsail 環境的測試,也可以另外加 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

如果噴掉的話可能要檢查你的 .envup 之前是否設定正確;但是基本上應該是正常 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 提供的會有限制的分鐘數(或是花錢),如果用自己的機器則就不限,而用法也很簡單,一樣先到設定裡面:

  1. 找到設定頁面 github-actions-runner-tab

  2. 點擊新增 github-actions-runner-new

基本上就選好自己的作業系統,然後跟著上面精簡的文件做操作就可以了,設定好就會看到他有被列出來。

之後如果你要用的話就要把 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



最後更新時間: 2022年08月16日.