--- title: "CI/CD Patterns with GitHub Actions" date: 2026-05-15T09:00:00+08:00 draft: false description: "Build matrices, caching strategies, environment-specific deployments, and reusable workflows. Patterns that scale from solo projects to team repos." tags: - CI/CD - GitHub - DevOps --- GitHub Actions is now the default CI for most open-source projects. Build matrices save config lines, caching `node_modules` and Go modules saves minutes per run. Reusable workflows keep things DRY across repos. ## Build Matrices Don't copy-paste job definitions for different Node versions or OS targets. Use a matrix: ```yaml jobs: test: strategy: matrix: node: [18, 20, 22] os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: npm test ``` This single definition expands to 6 parallel jobs. Add `fail-fast: false` if you want all runs to complete even when one fails — helpful for catching platform-specific bugs in one CI run. ## Smart Caching Caching is the easiest way to cut CI minutes: ```yaml - uses: actions/cache@v4 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }} restore-keys: npm-${{ runner.os }}- ``` The `restore-keys` fallback is important: even a partial cache hit saves time. For Go modules, cache `~/go/pkg/mod`. For Docker builds, use BuildKit's registry cache (`--cache-from type=registry`) for layer caching across CI runs. ## Environment-Specific Deployments Use environments to gate production deploys: ```yaml deploy-prod: needs: [test, build] environment: production steps: - run: ./deploy.sh production ``` The `environment: production` lets you set required reviewers, wait timers, and environment-specific secrets in the GitHub UI. No more accidentally deploying to prod from a feature branch. ## Reusable Workflows When you maintain multiple repos, reusable workflows prevent drift: ```yaml # .github/workflows/deploy.yml in a shared repo on: workflow_call: inputs: environment: required: true type: string secrets: AWS_ROLE: required: true ``` Call it from any repo: ```yaml jobs: deploy: uses: org/shared-workflows/.github/workflows/deploy.yml@main with: environment: staging secrets: AWS_ROLE: ${{ secrets.AWS_ROLE }} ``` Update the shared workflow once, and every repo benefits. This pattern scales well for orgs with 10+ services. ## Practical Tips - Set `timeout-minutes` on every job — the default is 360 minutes, and you don't want a hung test burning your quota. - Use `concurrency` to cancel redundant runs when pushing to the same PR multiple times. - Pin action versions to SHA hashes for supply-chain security, not just tags.