
這篇記錄把 GitHub Actions pipeline 從基礎補到可用: trigger 設計、path filter、concurrency、cache, 到 CI 自動更新 `deployment.yaml` 完成 GitOps 閉環。最後對比 Jenkins 和 CircleCI。

## Trigger 設計

三種 trigger 各自的用途：

```yaml
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        default: 'dev'
        type: choice
        options: [dev, prod]
```

| Trigger | 觸發時機 | 用途 |
|---|---|---|
| `push` | merge 到 main | 主線 CI, 每次合併後跑 |
| `pull_request` | PR 開啟或更新 | review 前驗證, 保護 main |
| `workflow_dispatch` | 手動從 GitHub UI | hotfix 補跑、手動指定環境 deploy |

`workflow_dispatch` 加了 `inputs` 之後, GitHub UI 會出現下拉選單, 讓你在觸發時選擇環境。`inputs` 支援 `string`、`boolean`、`number`、`choice` 四種類型。

## Path Filter：只在相關檔案變動時觸發

push README 不應該觸發 CI。`paths` filter 讓 workflow 只在有意義的檔案變動時才跑：

```yaml
on:
  push:
    branches: [main]
    paths:
      - '**.go'
      - 'Dockerfile'
      - 'go.mod'
      - 'go.sum'
      - '.github/workflows/**'
  pull_request:
    branches: [main]
    paths:
      - '**.go'
      - 'Dockerfile'
      - 'go.mod'
      - 'go.sum'
      - '.github/workflows/**'
```

`**` 匹配任意深度的子目錄, `*` 只匹配單層。`**.go` 能匹配 `handlers/health.go`、`middleware/auth.go`, 而 `*.go` 只匹配根目錄的 `.go` 檔。

`workflow_dispatch` 不需要 `paths`, 手動觸發永遠跑。

## `if` 條件：控制 Job 和 Step 執行

PR 的目的是 review, 不是上線。deploy job 不應該在 PR 觸發：

```yaml
deploy:
  needs: build
  if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
```

`github.event_name` 是 GitHub Actions 內建的 context, 值對應觸發的 trigger 名稱。`pull_request` 被排除在外, test 和 build 還是會跑, 確認 PR 可以合。

`if` 可以放在兩個層級：

| 層級 | 效果 | 範例 |
|---|---|---|
| job 層級 | 整個 job 跳過或執行 | deploy 只在 push 跑 |
| step 層級 | 單一 step 跳過或執行 | prod 環境才跑某個 step |

step 層級常見的內建函式：

```yaml
if: success()    # all previous steps passed (default)
if: failure()    # any previous step failed
if: always()     # runs regardless — cleanup, notifications
if: cancelled()  # workflow was cancelled
```

## Concurrency：防止同時跑多個 Deploy

```mermaid
sequenceDiagram
    participant C1 as Commit A
    participant C2 as Commit B
    participant D as Deploy Job

    C1->>D: trigger deploy (3 min)
    C2->>D: trigger deploy (30s later)
    Note over D: cancel-in-progress: true<br/>cancel A, run B
```

`concurrency` 讓同一組的 job 不能同時跑：

```yaml
deploy:
  concurrency:
    group: deploy-${{ github.ref }}
    cancel-in-progress: true
```

`group` 是識別 key, `github.ref` 是 branch 名稱, 讓 main branch 的 deploy 共用同一個 lock。

| | `cancel-in-progress: true` | `cancel-in-progress: false` |
|---|---|---|
| 行為 | 新的進來, 取消舊的 | 新的排隊等 |
| 適合 | deploy（要最新版本）| database migration（不能中斷）|

concurrency 只放在 deploy job, 不放 test 和 build——每個 commit 都需要完整的測試記錄。

## Go Module Cache：加速 CI

每次 runner 是全新環境, 所有 dependencies 重新下載。`go.sum` 沒變, 重新下載完全是浪費。

`actions/setup-go` v4 之後內建 cache：

```yaml
- uses: actions/setup-go@v6
  with:
    go-version-file: go.mod
    cache: true
```

`cache: true` 自動以 `runner.os + go.sum hash` 為 key。`go.sum` 沒變就命中 cache 跳過下載。

key 設計原則：**把決定 cache 內容的檔案 hash 進 key**。

| 情境 | Key 包含什麼 |
|---|---|
| Go | `runner.os` + `hashFiles('**/go.sum')` |
| Node.js | `runner.os` + `hashFiles('**/package-lock.json')` |
| Python | `runner.os` + `hashFiles('**/requirements.txt')` |

## ENV_TAG：環境感知的 Image Tag

```mermaid
flowchart LR
    A{trigger?} -->|workflow_dispatch| B[inputs.environment]
    A -->|push to main| C[prod]
    A -->|other branch| D[dev]
    B --> E[ENV_TAG]
    C --> E
    D --> E
```

```yaml
env:
  ENV_TAG: ${{ inputs.environment || (github.ref == 'refs/heads/main' && 'prod') || 'dev' }}
```

## SHORT_SHA：跨 Step 共用變數

每個 `run:` block 是獨立的 shell, 變數不能直接跨 step 傳遞。`GITHUB_ENV` 是 GitHub Actions 的特殊檔案, 寫進去的變數在後續所有 step 都能讀到：

```yaml
- name: Set SHORT_SHA
  run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV

- name: Docker push to AR
  run: |
    docker build -t ${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/go-api/go-api:${ENV_TAG}-${SHORT_SHA} .
    docker push ${GCP_REGION}-docker.pkg.dev/${GCP_PROJECT_ID}/go-api/go-api:${ENV_TAG}-${SHORT_SHA}
```

`${GITHUB_SHA::7}` 是 bash 字串截斷語法, 取前 7 字元。Docker push 和 deployment.yaml 更新都用同一個 `SHORT_SHA`, 保證 tag 一致。

## GitOps 自動更新 deployment.yaml

CI build 完之後, 自動更新 `k8s/base/deployment.yaml` 的 image tag, push 到 gitops branch, ArgoCD 偵測到變更後 sync：

```yaml
- name: Update image tag
  run: |
    sed -i "s|image: .*-docker.pkg.dev/.*/go-api/go-api:.*|image: us-east1-docker.pkg.dev/${GCP_PROJECT_ID}/go-api/go-api:${ENV_TAG}-${SHORT_SHA}|" k8s/base/deployment.yaml
    git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
    git config user.name "github-actions[bot]"
    git checkout -b gitops/update-image-${SHORT_SHA}
    git add k8s/base/deployment.yaml
    git commit -m "chore: update image tag to ${ENV_TAG}-${SHORT_SHA}"
    git push origin gitops/update-image-${SHORT_SHA}
```

`sed` 用 `|` 而不是 `/` 作為分隔符, 是因為 image URL 裡有 `/`, 避免語法混淆。push 到獨立的 gitops branch 而不是直接 push main, 是因為 main 有 branch protection rule。`contents: write` permission 讓 runner 有寫入 repo 的權限。

## Image Tagging：git SHA vs Semver

| | Git SHA | Semver (`v1.2.3`) |
|---|---|---|
| 唯一性 | 天生唯一 | 需要人工維護 |
| 可追蹤性 | 直接對應 commit | 需要額外記錄 tag 對應的 commit |
| 語意 | 無 | 有, `v1.3.0` vs `v1.2.1` 一眼看出 |
| 適合 | 內部服務、CI/CD 自動化 | 對外 API、library |

pipeline 用 `{env}-{sha}` 格式（例如 `prod-7639a24`）, 一眼看出環境和 commit。

## Jenkins vs GitHub Actions

```groovy
pipeline {
    agent any
    environment {
        GCP_REGION = 'us-east1'
    }
    stages {
        stage('Test') {
            steps { sh 'go test ./...' }
        }
        stage('Build') {
            steps { sh 'go build ./...' }
        }
        stage('Deploy') {
            when { branch 'main' }
            steps { sh 'docker build -t go-api .' }
        }
    }
    post {
        failure { echo 'Pipeline failed' }
        always { cleanWs() }
    }
}
```

| | GitHub Actions | Jenkins |
|---|---|---|
| 設定檔 | `.github/workflows/*.yml` | `Jenkinsfile` |
| 語法 | YAML | Groovy DSL |
| Job 依賴 | `needs: [test]` | stage 預設循序執行 |
| Branch 限制 | `if: github.event_name == 'push'` | `when { branch 'main' }` |
| Cleanup | step-level `if: always()` | `post { always {} }` |
| 架設 | 雲端, 不用管 infra | 需要自己維護 server |
| 整合 | GitHub 深度整合 | plugin 生態豐富 |
| 適合 | 雲端原生、開源專案 | 企業、on-premise、複雜 pipeline |

## CircleCI 對比

```yaml
# .circleci/config.yml
version: 2.1

orbs:
  go: circleci/go@1.11

workflows:
  ci:
    jobs:
      - test
      - build:
          requires: [test]
      - deploy:
          requires: [build]
          context: gcp-prod
          filters:
            branches:
              only: main

jobs:
  test:
    docker:
      - image: cimg/go:1.23
    steps:
      - checkout
      - go/load-cache
      - run: go test ./...
      - go/save-cache
```

| 概念 | GitHub Actions | CircleCI |
|---|---|---|
| Job 依賴 | `needs: [test]` | `requires: [test]` |
| Branch 限制 | `if:` condition | `filters: branches: only:` |
| 可重用套件 | Marketplace actions | Orbs |
| 跨 repo 共用 credentials | 每個 repo 各設 secrets | Contexts（org 層級共用）|
| 手動觸發 + 參數 | `workflow_dispatch` + UI | API call + `pipeline.parameters` |

CircleCI 的核心優勢是 Contexts 跨 repo 共用——多個 repo 指向同一個 Context, credentials 集中管理, 改一次全部生效。這個優勢在中大型企業多 repo 環境才有明顯差異。

## References

- [GitHub Actions Workflow Syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
- [GitHub Actions Contexts](https://docs.github.com/en/actions/learn-github-actions/contexts)
- [GitHub Actions GITHUB_ENV](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-environment-variable)
- [actions/setup-go cache](https://github.com/actions/setup-go#caching-dependency-files-and-build-outputs)
- [Jenkins Pipeline Documentation](https://www.jenkins.io/doc/book/pipeline/)
- [CircleCI Configuration Reference](https://circleci.com/docs/configuration-reference/)
- [CircleCI Contexts](https://circleci.com/docs/contexts/)
- [Semantic Versioning](https://semver.org/)
