LabHub

Blog

GitHub Actions Advanced CI/CD: Matrix Builds, Cache Strategies, and Security Hardening

한국어English日本語

Introduction: Why Advanced GitHub Actions Optimization Now

As of 2026, GitHub Actions has become the most widely used CI/CD platform in the world. According to GitHub's own announcements, more than 90% of Fortune 100 companies use GitHub Actions, and millions of workflows run every day. Most teams, however, stop at a basic build-test-deploy pipeline.

A simple pipeline is not enough in production. Multi-platform support, build time optimization, secret management, and supply chain security all have to be taken into account. The tj-actions/changed-files action supply chain attack that occurred in late 2025 in particular was a fresh reminder of how important security hardening is for GitHub Actions.

This article covers the advanced techniques for building a production-grade CI/CD pipeline, from matrix build optimization, cache strategies, reusable workflows, and OIDC-based authentication through to security hardening.

Primary Sources and Official Documentation

SourceDescription
GitHub Actions official documentationFull reference for workflow syntax, events, runners
GitHub Blog - Actions Security Best PracticesSupply chain security and action security best practices
OpenID Connect in GitHub ActionsOfficial guide to OIDC token based cloud authentication
GitHub Actions - Caching DependenciesOfficial guide to dependency caching strategies
Reusable WorkflowsOfficial documentation on workflow reuse patterns
GitHub Actions Runner - Self-hostedGuide to setting up and operating self-hosted runners

1. Matrix Build Optimization

1.1 Basic Matrix Strategy

A matrix strategy is the core feature for running a workflow in parallel across several environment combinations. Combining OS, language version, dependency version and so on lets you automate cross-testing.

name: Matrix CI
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      max-parallel: 6
      matrix:
        os: [ubuntu-22.04, ubuntu-24.04, macos-14]
        node-version: [20, 22]
        include:
          - os: ubuntu-24.04
            node-version: 22
            coverage: true
        exclude:
          - os: macos-14
            node-version: 20

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - run: npm ci
      - run: npm test

      - name: Upload coverage
        if: ${{ matrix.coverage }}
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

In the configuration above, fail-fast: false keeps the remaining combinations running even when one matrix combination fails. That way you can see in a single run which environments have problems.

1.2 Generating a Matrix Dynamically

Deciding what to test dynamically based on which files changed saves a great deal of build time in a large monorepo.

jobs:
  detect-changes:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - name: Detect changed services
        id: set-matrix
        run: |
          CHANGED=$(git diff --name-only HEAD~1 HEAD | grep -oP '^services/\K[^/]+' | sort -u | jq -R . | jq -s .)
          if [ "$CHANGED" = "[]" ]; then
            echo "matrix={\"service\":[\"dummy\"]}" >> $GITHUB_OUTPUT
          else
            echo "matrix={\"service\":$CHANGED}" >> $GITHUB_OUTPUT
          fi

  build:
    needs: detect-changes
    if: ${{ fromJson(needs.detect-changes.outputs.matrix).service[0] != 'dummy' }}
    runs-on: ubuntu-latest
    strategy:
      matrix: ${{ fromJson(needs.detect-changes.outputs.matrix) }}
    steps:
      - uses: actions/checkout@v4
      - name: Build service
        run: |
          echo "Building ${{ matrix.service }}"
          cd services/${{ matrix.service }}
          docker build -t ${{ matrix.service }}:${{ github.sha }} .

1.3 Matrix Optimization Tips


2. Cache Strategies in Depth

2.1 Comparison by Cache Type

Cache strategyStrengthsWeaknessesWhere it fits
actions/cacheGeneral purpose, fine-grained controlManual key managementCustom build tooling
setup-node cache optionSimple setup, automatic key creationNode.js onlyNode.js projects
Docker layer cachingShortens image build timeCache size limit (10GB)Container builds
Artifact cachingPasses data between jobsLimited to a single workflowSharing build output

2.2 Advanced Cache Configuration

An effective cache key strategy can cut build time by 50~70%.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Multi-stage cache restore strategy
      - name: Cache node_modules
        uses: actions/cache@v4
        id: npm-cache
        with:
          path: |
            node_modules
            ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      # Next.js build cache
      - name: Cache Next.js build
        uses: actions/cache@v4
        with:
          path: .next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
            ${{ runner.os }}-nextjs-

      # Gradle cache (Java/Kotlin projects)
      - name: Cache Gradle packages
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Install dependencies
        if: steps.npm-cache.outputs.cache-hit != 'true'
        run: npm ci

      - name: Build
        run: npm run build

2.3 Docker Build Cache Optimization

Using the BuildKit cache in Docker image builds can shorten build time considerably.

jobs:
  docker-build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push with cache
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

Using cache-from: type=gha and cache-to: type=gha,mode=max caches Docker layers through the GitHub Actions cache backend. mode=max caches intermediate layers as well, which maximizes the speed of subsequent builds.

2.4 Cache Management Caveats


3. Reusable Workflows and Composite Actions

3.1 Reusable Workflow

To share a standardized CI/CD pipeline across the whole organization, use a reusable workflow. On the calling side you reference a workflow in another repository with the uses keyword.

The called workflow (.github/workflows/reusable-deploy.yml):

name: Reusable Deploy Workflow

on:
  workflow_call:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: string
      image-tag:
        description: 'Docker image tag'
        required: true
        type: string
    secrets:
      KUBE_CONFIG:
        required: true
    outputs:
      deploy-url:
        description: 'Deployed URL'
        value: ${{ jobs.deploy.outputs.url }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    outputs:
      url: ${{ steps.deploy.outputs.url }}
    steps:
      - uses: actions/checkout@v4

      - name: Configure kubectl
        run: |
          echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
          export KUBECONFIG=kubeconfig

      - name: Deploy to Kubernetes
        id: deploy
        run: |
          kubectl set image deployment/app \
            app=${{ inputs.image-tag }} \
            -n ${{ inputs.environment }}
          kubectl rollout status deployment/app \
            -n ${{ inputs.environment }} --timeout=300s
          URL=$(kubectl get ingress app -n ${{ inputs.environment }} -o jsonpath='{.spec.rules[0].host}')
          echo "url=https://$URL" >> $GITHUB_OUTPUT

The calling workflow:

name: Production Deploy

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        id: meta
        run: |
          TAG="ghcr.io/${{ github.repository }}:${{ github.ref_name }}"
          docker build -t $TAG .
          docker push $TAG
          echo "tags=$TAG" >> $GITHUB_OUTPUT

  deploy-staging:
    needs: build
    uses: my-org/shared-workflows/.github/workflows/reusable-deploy.yml@v2
    with:
      environment: staging
      image-tag: ${{ needs.build.outputs.image-tag }}
    secrets:
      KUBE_CONFIG: ${{ secrets.STAGING_KUBE_CONFIG }}

  deploy-production:
    needs: [build, deploy-staging]
    uses: my-org/shared-workflows/.github/workflows/reusable-deploy.yml@v2
    with:
      environment: production
      image-tag: ${{ needs.build.outputs.image-tag }}
    secrets:
      KUBE_CONFIG: ${{ secrets.PROD_KUBE_CONFIG }}

3.2 Composite Action

A composite action bundles several steps into a single reusable action. Unlike a reusable workflow, it is reused at the step level rather than the job level.

# .github/actions/setup-and-test/action.yml
name: 'Setup and Test'
description: 'Install dependencies, lint, and test'

inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '22'
  working-directory:
    description: 'Working directory for the project'
    required: false
    default: '.'

outputs:
  coverage-percentage:
    description: 'Test coverage percentage'
    value: ${{ steps.coverage.outputs.percentage }}

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
        cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json

    - name: Install dependencies
      shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: npm ci

    - name: Lint
      shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: npm run lint

    - name: Test with coverage
      shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: npm test -- --coverage --coverageReporters=text-summary

    - name: Extract coverage
      id: coverage
      shell: bash
      run: |
        COVERAGE=$(cat ${{ inputs.working-directory }}/coverage/coverage-summary.json | jq '.total.lines.pct')
        echo "percentage=$COVERAGE" >> $GITHUB_OUTPUT

3.3 Reusable Workflow vs Composite Action

AspectReusable WorkflowComposite Action
Unit of reuseAn entire jobA single step
How it is calledjobs.*.usessteps.*.uses
Passing secretsMust be passed explicitlyInherits the caller's context
Nested callsUp to 4 levelsUp to 10 levels
Runner selectionSpecified inside the workflowUses the caller's runner
Where it fitsStandardizing a whole pipelineShared setup/teardown steps

4. GitHub-hosted vs Self-hosted Runner Comparison

4.1 Comparison Table

AspectGitHub-hosted RunnerSelf-hosted Runner
CostBilled per minute (Linux 0.008 USD/min)Only infrastructure cost
EnvironmentA clean VM every timePersistent environment (cache can be kept)
CustomizationLimited (preinstalled tools only)Completely free (GPU, special hardware)
SecurityManaged by GitHubManaged by the organization itself
NetworkPublic internetCan reach private networks
ScalingAutomaticManual, or needs an autoscaling setup
Concurrency limitVaries by planControlled directly
MaintenanceNot neededOS patching and runner updates needed

4.2 Self-hosted Runner Security Caveats

Do not use a self-hosted runner on a public repository. Malicious code in a forked PR can run on the runner. Use them only on private repositories, or run ephemeral runners with a tool such as Actions Runner Controller (ARC).

# Actions Runner Controller (ARC) - Kubernetes-based autoscaling self-hosted runners
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: runner-deployment
spec:
  replicas: 3
  template:
    spec:
      repository: my-org/my-repo
      ephemeral: true
      labels:
        - self-hosted
        - linux
        - x64
        - gpu
      resources:
        limits:
          nvidia.com/gpu: 1
          memory: '16Gi'
        requests:
          cpu: '4'
          memory: '8Gi'
---
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
  name: runner-autoscaler
spec:
  scaleTargetRef:
    kind: RunnerDeployment
    name: runner-deployment
  minReplicas: 1
  maxReplicas: 10
  metrics:
    - type: TotalNumberOfQueuedAndInProgressWorkflowRuns
      repositoryNames:
        - my-org/my-repo

5. OIDC-based Cloud Authentication (the Core of Security Hardening)

5.1 Why OIDC

The traditional approach stored long-lived credentials such as AWS access keys or GCP service account keys in GitHub Secrets. That approach carries several risks.

With an OIDC (OpenID Connect) token, GitHub Actions authenticates to the cloud provider with a short-lived token. No long-lived credential is needed, so security improves considerably.

5.2 Setting Up AWS OIDC Authentication

name: Deploy to AWS
on:
  push:
    branches: [main]

permissions:
  id-token: write # Required to request an OIDC token
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          role-session-name: github-actions-${{ github.run_id }}
          aws-region: ap-northeast-2

      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster production \
            --service my-app \
            --force-new-deployment

      - name: Verify deployment
        run: |
          aws ecs wait services-stable \
            --cluster production \
            --services my-app

5.3 Setting Up GCP OIDC Authentication

jobs:
  deploy-gcp:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/github-pool/providers/github-provider'
          service_account: 'deploy@my-project.iam.gserviceaccount.com'

      - name: Deploy to Cloud Run
        uses: google-github-actions/deploy-cloudrun@v2
        with:
          service: my-app
          region: asia-northeast3
          image: gcr.io/my-project/my-app:${{ github.sha }}

6. A Complete Guide to Security Hardening

6.1 Applying the Principle of Least Privilege

The GITHUB_TOKEN in GitHub Actions has broad permissions by default. Only the permissions that are genuinely needed should be declared explicitly.

name: Secure Workflow

# Minimize every permission at the global level
permissions: {}

on:
  pull_request:
    branches: [main]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    # Declare only the permissions needed at the job level
    permissions:
      contents: read
      checks: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

6.2 Pinning Action Versions (Supply Chain Security)

Pin an action with a commit SHA instead of a tag (v4). A tag can be moved maliciously, but a commit SHA cannot be changed.

steps:
  # Risky: a tag can be moved
  # - uses: actions/checkout@v4

  # Safe: pinned to a commit SHA
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

  - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
    with:
      node-version: '22'

  - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
    with:
      path: node_modules
      key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

Dependabot, which GitHub provides, can manage this SHA pinning automatically. Add the following to .github/dependabot.yml.

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: 'github-actions'
    directory: '/'
    schedule:
      interval: 'weekly'
    groups:
      actions:
        patterns:
          - '*'

6.3 Secret Management Best Practices

jobs:
  secure-deploy:
    runs-on: ubuntu-latest
    environment: production # Environment Protection Rules applied
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      # Caveats when exposing secrets as environment variables
      - name: Deploy with secrets
        env:
          # Pass only individual secrets, explicitly
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}
        run: |
          # Mask so secrets are not exposed in the log
          echo "::add-mask::$DATABASE_URL"
          echo "::add-mask::$API_KEY"
          ./deploy.sh

6.4 Fork PR Security Settings

Pull requests coming from a forked repository carry a high security risk. The following settings are recommended.

jobs:
  test:
    # Run tests safely even on fork PRs
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - run: npm ci
      - run: npm test

  deploy-preview:
    # Does not run on fork PRs
    if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - name: Deploy preview
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: ./deploy-preview.sh

6.5 Artifact Attestation (Proving Build Provenance)

GitHub Actions supports the SLSA (Supply-chain Levels for Software Artifacts) framework, so the provenance of build output can be attested.

jobs:
  build-with-attestation:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      attestations: write
      packages: write
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Build Docker image
        run: |
          docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .

      - name: Push to GHCR
        run: |
          echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

      - name: Generate artifact attestation
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ghcr.io/${{ github.repository }}
          subject-digest: sha256:${{ steps.build.outputs.digest }}
          push-to-registry: true

7. A Real-World Integrated Workflow: the Production Pipeline

The following is an example of a production-grade CI/CD pipeline that combines every technique covered so far.

name: Production CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# Global least privilege
permissions:
  contents: read

# Cancel the previous run on the same branch
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check

  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --shard=${{ matrix.shard }}/4
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results-${{ matrix.shard }}
          path: test-results/
          retention-days: 7

  security-scan:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          scan-ref: '.'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
      - name: Upload scan results
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

  build-and-push:
    needs: [lint, test, security-scan]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
      id-token: write
      attestations: write
    outputs:
      image-digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Attest build provenance
        uses: actions/attest-build-provenance@v2
        with:
          subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          subject-digest: ${{ steps.build.outputs.digest }}
          push-to-registry: true

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    environment: production
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          aws-region: ap-northeast-2

      - name: Deploy to EKS
        run: |
          aws eks update-kubeconfig --name production-cluster
          kubectl set image deployment/app \
            app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          kubectl rollout status deployment/app --timeout=300s

8. Operational Caveats and Failure Cases

8.1 Common Failure Cases

Case 1: cache poisoning attack

In public repositories there have been cases where a fork PR injected a malicious dependency into the cache, and later builds used that cache and executed the malicious code. As a countermeasure, restrict cache writes on the pull_request event and include branch information in the cache key.

Case 2: secrets exposed in the log

A case where secrets ended up in the log while printing environment variables for debugging. GitHub Actions masks registered secrets automatically, but printing a secret after transforming it (base64 encoding and so on) bypasses the masking. Transformed values must also be masked with the echo "::add-mask::" command.

Case 3: third-party action tag tampering

In the tj-actions/changed-files supply chain attack in late 2025, the attacker repointed the action's tag to a commit containing malicious code. Anyone using a commit SHA instead of a tag would not have been affected.

Case 4: duplicate deploys caused by not setting concurrency

A case where rapid successive pushes let several deploy jobs run at the same time, making rollback complicated. A concurrency group has to be set to prevent simultaneous deploys to the same environment.

8.2 Cost Optimization Tips


9. Production CI/CD Checklist

Security

Performance

Maintenance

Supply Chain Security


Conclusion

Using the advanced features of GitHub Actions, you can go beyond simple build/test automation and build a CI/CD pipeline with production-grade security and efficiency. Three points in particular are worth keeping in mind.

  1. Security first: the principle of least privilege, SHA pinning and OIDC authentication are requirements, not options
  2. Cache strategy: the right cache configuration alone can cut build time by more than 50%
  3. Modularization: reusable workflows and composite actions raise maintainability and establish a standard across the organization

A CI/CD pipeline is not something you build once and are done with; it is a living system that has to be improved and hardened continuously. Reviewing the checklist regularly and responding to new security threats matters.


References

Comments

No comments yet.

Sign in to leave a comment