> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go-mizu.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# CI/CD Pipelines

> Automate testing, building, and deploying Mizu applications with GitHub Actions, GitLab CI, and other CI/CD platforms.

**CI/CD** stands for **Continuous Integration** and **Continuous Deployment**. These practices automate the tedious parts of software development:

* **Continuous Integration (CI)**: Automatically runs tests and builds your code every time you push changes. Catches bugs early, before they reach production.
* **Continuous Deployment (CD)**: Automatically deploys your code to staging or production after tests pass. No manual SSH or copy-paste deployments.

Without CI/CD, deploying looks like: run tests locally, build the binary, copy it to the server, restart the service. With CI/CD, you just push to git—everything else happens automatically.

This guide covers setting up pipelines for Mizu applications using popular CI/CD platforms.

## GitHub Actions

**GitHub Actions** is GitHub's built-in CI/CD platform. It's free for public repositories and has generous free limits for private repos. You define workflows in YAML files in the `.github/workflows/` directory.

### Basic CI Pipeline

This pipeline runs on every push and pull request. It tests your code, runs linting, and builds the binary to verify everything compiles:

`.github/workflows/ci.yml`:

```yaml theme={null}
name: CI

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

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

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Download dependencies
        run: go mod download

      - name: Run tests
        run: go test -v -race -coverprofile=coverage.out ./...

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          files: coverage.out

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

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Run golangci-lint
        uses: golangci/golangci-lint-action@v4
        with:
          version: latest

  build:
    runs-on: ubuntu-latest
    needs: [test, lint]
    steps:
      - uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Build
        run: |
          CGO_ENABLED=0 go build -ldflags="-s -w" -o myapp ./cmd/server
```

### Complete CI/CD Pipeline

This comprehensive pipeline demonstrates a full CI/CD setup: test, build a Docker image, push to a registry, and deploy to staging (on push to main) or production (on version tags).

The `needs` keyword creates dependencies between jobs—build only runs after test passes, deploy only runs after build succeeds.

`.github/workflows/deploy.yml`:

```yaml theme={null}
name: Deploy

on:
  push:
    branches: [main]
    tags: ['v*']

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

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

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Test
        run: go test -v -race ./...

  build-and-push:
    runs-on: ubuntu-latest
    needs: test
    permissions:
      contents: read
      packages: write
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4

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

      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.ref == 'refs/heads/main'
    environment: staging
    steps:
      - name: Deploy to staging
        run: |
          # Deploy using your preferred method
          echo "Deploying to staging..."

  deploy-production:
    runs-on: ubuntu-latest
    needs: build-and-push
    if: startsWith(github.ref, 'refs/tags/v')
    environment: production
    steps:
      - name: Deploy to production
        run: |
          echo "Deploying to production..."
```

### Multi-Architecture Builds

Build for both AMD64 and ARM64:

```yaml theme={null}
- name: Set up QEMU
  uses: docker/setup-qemu-action@v3

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

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    platforms: linux/amd64,linux/arm64
    cache-from: type=gha
    cache-to: type=gha,mode=max
```

## GitLab CI/CD

**GitLab CI/CD** is built into GitLab and uses a single `.gitlab-ci.yml` file at the repository root. It's similar to GitHub Actions but with different syntax. GitLab also includes a container registry, making it convenient for Docker-based workflows.

### Basic Pipeline

`.gitlab-ci.yml`:

```yaml theme={null}
stages:
  - test
  - build
  - deploy

variables:
  GO_VERSION: "1.22"

default:
  image: golang:${GO_VERSION}

test:
  stage: test
  script:
    - go mod download
    - go test -v -race -coverprofile=coverage.out ./...
  coverage: '/coverage: \d+.\d+% of statements/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

lint:
  stage: test
  image: golangci/golangci-lint:latest
  script:
    - golangci-lint run

build:
  stage: build
  script:
    - CGO_ENABLED=0 go build -ldflags="-s -w" -o myapp ./cmd/server
  artifacts:
    paths:
      - myapp
    expire_in: 1 week

build-docker:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - |
      if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
        docker push $CI_REGISTRY_IMAGE:latest
      fi

deploy-staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.example.com
  script:
    - echo "Deploying to staging..."
  only:
    - main

deploy-production:
  stage: deploy
  environment:
    name: production
    url: https://example.com
  script:
    - echo "Deploying to production..."
  only:
    - tags
  when: manual
```

### Kaniko for Docker Builds

Build without Docker-in-Docker (more secure):

```yaml theme={null}
build-docker:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:v1.21.0-debug
    entrypoint: [""]
  script:
    - |
      /kaniko/executor \
        --context $CI_PROJECT_DIR \
        --dockerfile $CI_PROJECT_DIR/Dockerfile \
        --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
        --destination $CI_REGISTRY_IMAGE:latest \
        --cache=true
```

## Deploying to Servers via SSH

### GitHub Actions

```yaml theme={null}
deploy:
  runs-on: ubuntu-latest
  needs: build
  steps:
    - uses: actions/checkout@v4

    - name: Build binary
      run: |
        CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
          go build -ldflags="-s -w" -o myapp ./cmd/server

    - name: Deploy to server
      uses: appleboy/scp-action@v0.1.7
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USER }}
        key: ${{ secrets.SSH_KEY }}
        source: myapp
        target: /tmp/

    - name: Restart service
      uses: appleboy/ssh-action@v1.0.3
      with:
        host: ${{ secrets.SSH_HOST }}
        username: ${{ secrets.SSH_USER }}
        key: ${{ secrets.SSH_KEY }}
        script: |
          sudo systemctl stop myapp
          sudo mv /tmp/myapp /usr/local/bin/myapp
          sudo chmod +x /usr/local/bin/myapp
          sudo systemctl start myapp

          # Wait for health check
          sleep 5
          curl -sf http://localhost:3000/readyz || exit 1
```

### GitLab CI

```yaml theme={null}
deploy:
  stage: deploy
  before_script:
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_ed25519
    - chmod 600 ~/.ssh/id_ed25519
    - ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
  script:
    - scp myapp $SSH_USER@$SSH_HOST:/tmp/myapp
    - |
      ssh $SSH_USER@$SSH_HOST << 'EOF'
        sudo systemctl stop myapp
        sudo mv /tmp/myapp /usr/local/bin/myapp
        sudo chmod +x /usr/local/bin/myapp
        sudo systemctl start myapp
      EOF
```

## Deploying to Kubernetes

### GitHub Actions with kubectl

```yaml theme={null}
deploy-kubernetes:
  runs-on: ubuntu-latest
  needs: build-and-push
  steps:
    - uses: actions/checkout@v4

    - name: Set up kubectl
      uses: azure/setup-kubectl@v3

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

    - name: Deploy
      run: |
        # Update image tag
        kubectl set image deployment/myapp \
          myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
          -n myapp

        # Wait for rollout
        kubectl rollout status deployment/myapp -n myapp --timeout=300s
```

### Using Kustomize

`k8s/base/kustomization.yaml`:

```yaml theme={null}
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
  - ingress.yaml
```

`k8s/overlays/production/kustomization.yaml`:

```yaml theme={null}
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: myapp-prod
resources:
  - ../../base
images:
  - name: myapp
    newName: ghcr.io/myorg/myapp
    newTag: latest
replicas:
  - name: myapp
    count: 3
```

GitHub Actions:

```yaml theme={null}
deploy:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Set up kubectl
      uses: azure/setup-kubectl@v3

    - name: Deploy with Kustomize
      run: |
        cd k8s/overlays/production
        kustomize edit set image myapp=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
        kubectl apply -k .
```

### Using Helm

```yaml theme={null}
deploy:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Set up Helm
      uses: azure/setup-helm@v3

    - name: Deploy with Helm
      run: |
        helm upgrade --install myapp ./charts/myapp \
          --namespace myapp \
          --set image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} \
          --set image.tag=${{ github.sha }} \
          --wait --timeout 5m
```

## Deploying to Cloud Platforms

### AWS App Runner

```yaml theme={null}
deploy-app-runner:
  runs-on: ubuntu-latest
  needs: build-and-push
  steps:
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-1

    - name: Deploy to App Runner
      run: |
        aws apprunner start-deployment \
          --service-arn ${{ secrets.APP_RUNNER_SERVICE_ARN }}
```

### AWS ECS

```yaml theme={null}
deploy-ecs:
  runs-on: ubuntu-latest
  needs: build-and-push
  steps:
    - uses: actions/checkout@v4

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-east-1

    - name: Login to ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v2

    - name: Update task definition
      id: task-def
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      with:
        task-definition: ecs/task-definition.json
        container-name: myapp
        image: ${{ steps.login-ecr.outputs.registry }}/myapp:${{ github.sha }}

    - name: Deploy to ECS
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: ${{ steps.task-def.outputs.task-definition }}
        service: myapp
        cluster: myapp-cluster
        wait-for-service-stability: true
```

### Google Cloud Run

```yaml theme={null}
deploy-cloud-run:
  runs-on: ubuntu-latest
  needs: build-and-push
  steps:
    - name: Authenticate to Google Cloud
      uses: google-github-actions/auth@v2
      with:
        credentials_json: ${{ secrets.GCP_SA_KEY }}

    - name: Set up Cloud SDK
      uses: google-github-actions/setup-gcloud@v2

    - name: Deploy to Cloud Run
      run: |
        gcloud run deploy myapp \
          --image gcr.io/${{ secrets.GCP_PROJECT }}/myapp:${{ github.sha }} \
          --region us-central1 \
          --platform managed \
          --allow-unauthenticated
```

### Fly.io

```yaml theme={null}
deploy-fly:
  runs-on: ubuntu-latest
  needs: test
  steps:
    - uses: actions/checkout@v4

    - name: Set up Fly
      uses: superfly/flyctl-actions/setup-flyctl@master

    - name: Deploy to Fly
      run: flyctl deploy --remote-only
      env:
        FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
```

### Railway

```yaml theme={null}
deploy-railway:
  runs-on: ubuntu-latest
  needs: test
  steps:
    - uses: actions/checkout@v4

    - name: Deploy to Railway
      uses: bervProject/railway-deploy@main
      with:
        railway_token: ${{ secrets.RAILWAY_TOKEN }}
        service: myapp
```

### DigitalOcean App Platform

```yaml theme={null}
deploy-digitalocean:
  runs-on: ubuntu-latest
  needs: build-and-push
  steps:
    - name: Install doctl
      uses: digitalocean/action-doctl@v2
      with:
        token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}

    - name: Deploy to App Platform
      run: |
        doctl apps create-deployment ${{ secrets.APP_ID }}
```

## Secrets Management

Your CI/CD pipelines need access to secrets like API keys, database passwords, and SSH keys for deployment. **Never commit secrets to your repository**—even in private repos, they can be exposed through logs, forks, or accidental public exposure.

CI/CD platforms provide secure ways to store and access secrets. They're encrypted at rest and only exposed to your pipelines as environment variables.

### GitHub Actions Secrets

GitHub Secrets are stored encrypted and exposed only to workflows. Add them in your repository's Settings → Secrets and variables → Actions:

```yaml theme={null}
env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  API_KEY: ${{ secrets.API_KEY }}
```

### Using Environment Files

For multiple environment variables:

```yaml theme={null}
- name: Create env file
  run: |
    cat > .env << EOF
    DATABASE_URL=${{ secrets.DATABASE_URL }}
    API_KEY=${{ secrets.API_KEY }}
    REDIS_URL=${{ secrets.REDIS_URL }}
    EOF

- name: Deploy with env
  run: |
    scp .env $SSH_USER@$SSH_HOST:/etc/myapp/env
```

### HashiCorp Vault

```yaml theme={null}
- name: Import secrets from Vault
  uses: hashicorp/vault-action@v2
  with:
    url: https://vault.example.com
    token: ${{ secrets.VAULT_TOKEN }}
    secrets: |
      secret/data/myapp DATABASE_URL | DATABASE_URL ;
      secret/data/myapp API_KEY | API_KEY

- name: Use secrets
  run: |
    echo "Database: $DATABASE_URL"
```

### AWS Secrets Manager

```yaml theme={null}
- name: Get secrets from AWS
  uses: aws-actions/aws-secretsmanager-get-secrets@v1
  with:
    secret-ids: |
      myapp/production
    parse-json-secrets: true

- name: Use secrets
  run: |
    echo "Using $MYAPP_PRODUCTION_DATABASE_URL"
```

## Release Automation

### Semantic Versioning with Release Please

`.github/workflows/release.yml`:

```yaml theme={null}
name: Release

on:
  push:
    branches: [main]

permissions:
  contents: write
  pull-requests: write

jobs:
  release:
    runs-on: ubuntu-latest
    outputs:
      release_created: ${{ steps.release.outputs.release_created }}
      tag_name: ${{ steps.release.outputs.tag_name }}
    steps:
      - uses: google-github-actions/release-please-action@v4
        id: release
        with:
          release-type: go

  build-release:
    needs: release
    if: ${{ needs.release.outputs.release_created }}
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - goos: linux
            goarch: amd64
          - goos: linux
            goarch: arm64
          - goos: darwin
            goarch: amd64
          - goos: darwin
            goarch: arm64
          - goos: windows
            goarch: amd64
    steps:
      - uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Build
        env:
          GOOS: ${{ matrix.goos }}
          GOARCH: ${{ matrix.goarch }}
        run: |
          CGO_ENABLED=0 go build -ldflags="-s -w" \
            -o myapp-${{ matrix.goos }}-${{ matrix.goarch }} \
            ./cmd/server

      - name: Upload release assets
        uses: softprops/action-gh-release@v1
        with:
          tag_name: ${{ needs.release.outputs.tag_name }}
          files: myapp-*
```

### Using GoReleaser

`.goreleaser.yml`:

```yaml theme={null}
version: 2

builds:
  - main: ./cmd/server
    binary: myapp
    env:
      - CGO_ENABLED=0
    goos:
      - linux
      - darwin
      - windows
    goarch:
      - amd64
      - arm64
    ldflags:
      - -s -w
      - -X main.version={{.Version}}
      - -X main.commit={{.ShortCommit}}

archives:
  - format: tar.gz
    name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
    format_overrides:
      - goos: windows
        format: zip

dockers:
  - image_templates:
      - "ghcr.io/myorg/myapp:{{ .Tag }}"
      - "ghcr.io/myorg/myapp:latest"
    dockerfile: Dockerfile
    build_flag_templates:
      - "--platform=linux/amd64"

changelog:
  sort: asc
  filters:
    exclude:
      - '^docs:'
      - '^test:'
      - '^ci:'
```

GitHub Actions:

```yaml theme={null}
release:
  runs-on: ubuntu-latest
  if: startsWith(github.ref, 'refs/tags/')
  steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: '1.22'

    - name: Run GoReleaser
      uses: goreleaser/goreleaser-action@v5
      with:
        version: latest
        args: release --clean
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

## Testing Strategies

### Matrix Testing

Test across Go versions:

```yaml theme={null}
test:
  runs-on: ubuntu-latest
  strategy:
    matrix:
      go-version: ['1.21', '1.22', '1.23']
  steps:
    - uses: actions/checkout@v4

    - name: Set up Go ${{ matrix.go-version }}
      uses: actions/setup-go@v5
      with:
        go-version: ${{ matrix.go-version }}

    - name: Test
      run: go test -v ./...
```

### Integration Tests with Services

```yaml theme={null}
test:
  runs-on: ubuntu-latest
  services:
    postgres:
      image: postgres:16
      env:
        POSTGRES_PASSWORD: postgres
        POSTGRES_DB: test
      ports:
        - 5432:5432
      options: >-
        --health-cmd pg_isready
        --health-interval 10s
        --health-timeout 5s
        --health-retries 5

    redis:
      image: redis:7
      ports:
        - 6379:6379
      options: >-
        --health-cmd "redis-cli ping"
        --health-interval 10s
        --health-timeout 5s
        --health-retries 5

  steps:
    - uses: actions/checkout@v4

    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: '1.22'

    - name: Run tests
      env:
        DATABASE_URL: postgres://postgres:postgres@localhost:5432/test?sslmode=disable
        REDIS_URL: redis://localhost:6379
      run: go test -v -tags=integration ./...
```

## Best Practices

Following CI/CD best practices makes your pipelines faster, more reliable, and easier to maintain. Here are the most important ones:

### Cache Dependencies

Downloading dependencies on every build is slow and wastes resources. Most CI platforms support caching, which stores your `go.mod` packages between runs:

```yaml theme={null}
- uses: actions/setup-go@v5
  with:
    go-version: '1.22'
    cache: true  # Caches Go modules automatically
```

### Parallel Jobs

Run independent jobs in parallel:

```yaml theme={null}
jobs:
  test:
    runs-on: ubuntu-latest
    # ...

  lint:
    runs-on: ubuntu-latest
    # ...

  security:
    runs-on: ubuntu-latest
    # ...

  build:
    runs-on: ubuntu-latest
    needs: [test, lint, security]  # Waits for all to complete
```

### Environment Protection

Use GitHub environments for production:

```yaml theme={null}
deploy-production:
  runs-on: ubuntu-latest
  environment:
    name: production
    url: https://myapp.example.com
  # Requires approval before running
```

### Status Badges

Add to your README:

```markdown theme={null}
![CI](https://github.com/myorg/myapp/actions/workflows/ci.yml/badge.svg)
![Deploy](https://github.com/myorg/myapp/actions/workflows/deploy.yml/badge.svg)
```

## Complete Example

`.github/workflows/main.yml`:

```yaml theme={null}
name: CI/CD

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'
      - run: go test -v -race -coverprofile=coverage.out ./...
      - uses: codecov/codecov-action@v4
        with:
          files: coverage.out

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.22'
      - uses: golangci/golangci-lint-action@v4

  build-and-push:
    runs-on: ubuntu-latest
    needs: [test, lint]
    if: github.event_name == 'push'
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build-and-push
    if: github.ref == 'refs/heads/main'
    environment: staging
    steps:
      - name: Deploy
        run: echo "Deploying to staging..."

  deploy-production:
    runs-on: ubuntu-latest
    needs: build-and-push
    if: startsWith(github.ref, 'refs/tags/v')
    environment: production
    steps:
      - name: Deploy
        run: echo "Deploying to production..."
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Docker" icon="docker" href="/guides/deployment/docker">
    Containerize your application.
  </Card>

  <Card title="Kubernetes" icon="dharmachakra" href="/guides/deployment/kubernetes">
    Deploy to Kubernetes clusters.
  </Card>
</CardGroup>
