GitAutomated Deployment

Automated Deployment with Git

Automated deployment connects your Git workflow directly to production. Instead of manually copying files to a server, a push to main triggers a pipeline that builds, tests, and deploys — consistently, reliably, and without human error. This page covers deployment patterns from simple push-to-deploy to production protection rules and rollback.

Deployment trigger patterns

Trigger

Pattern

Best for

Push to main

Every merge to main deploys to production

High-frequency deploys, SaaS apps

Push to develop

Merges to develop deploy to staging

Staging environment validation

Tag creation

Push a version tag (v1.2.0) to deploy

Versioned releases, mobile apps, packages

Manual dispatch

Human clicks "Deploy" in the CI UI

Production with approval gates

Schedule

Deploy every night at 2am

Batch jobs, database maintenance

Webhook

External system triggers deploy

After CMS publishes, after data pipeline

Push-to-deploy (merge to main)

.github/workflows/deploy-vercel.yml

YAML
name: Deploy to Vercel

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      - name: Deploy to Vercel
        run: npx vercel --prod --token=$VERCEL_TOKEN
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
Tag-to-release deployment

Tagging a commit with a version number triggers deployment. This is the standard pattern for libraries, mobile apps, and applications that need explicit versioned releases.

Creating and pushing a release tag

Bash
# Create an annotated tag
git tag -a v1.2.0 -m "Release v1.2.0: add payment feature"

# Push the tag to remote (triggers the deploy workflow)
git push origin v1.2.0

# Or push all tags
git push origin --tags

.github/workflows/release.yml

YAML
name: Release

on:
  push:
    tags:
      - 'v*.*.*'   # matches v1.0.0, v2.3.1, etc.

jobs:
  release:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm test
      - run: npm run build

      - name: Get version from tag
        id: version
        run: echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT

      - name: Deploy version ${{ steps.version.outputs.VERSION }}
        run: ./scripts/deploy.sh ${{ steps.version.outputs.VERSION }}
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
Deploy to AWS S3 + CloudFront

.github/workflows/deploy-aws.yml

YAML
name: Deploy to AWS

on:
  push:
    branches: [main]

permissions:
  id-token: write   # for OIDC
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }

      - run: npm ci && npm run build

      # Authenticate to AWS using OIDC (no long-lived credentials)
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
          aws-region: us-east-1

      # Sync build output to S3
      - name: Upload to S3
        run: |
          aws s3 sync ./dist s3://${{ secrets.S3_BUCKET }} \
            --delete \
            --cache-control "max-age=31536000,immutable" \
            --exclude "*.html"

          # HTML files should not be cached aggressively
          aws s3 sync ./dist s3://${{ secrets.S3_BUCKET }} \
            --delete \
            --cache-control "no-cache" \
            --include "*.html" \
            --exclude "*"

      # Invalidate CloudFront so users get the new version
      - name: Invalidate CloudFront
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
            --paths "/*"
Staging before production: two-environment workflow

deploy to staging automatically, production manually

YAML
jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    needs: test
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - name: Deploy to staging
        run: ./deploy.sh staging
        env:
          DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}

  deploy-production:
    runs-on: ubuntu-latest
    environment: production    # configured with required reviewers
    needs: deploy-staging
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - name: Deploy to production
        run: ./deploy.sh production
        env:
          DEPLOY_KEY: ${{ secrets.PRODUCTION_DEPLOY_KEY }}
Warning
Always test in staging before deploying to production. Configure the `production` environment in GitHub (Settings → Environments → production) with "Required reviewers" so a human must approve before the production deploy job runs.
Environment protection rules
  1. Go to Settings → Environments → New environment → name it "production".

  2. Enable "Required reviewers" and add the names of who can approve deploys.

  3. Set "Deployment branches" to "Selected branches" → main only.

  4. Add any environment-specific secrets (production API keys, etc.).

  5. Optional: enable "Wait timer" (e.g. 10 minutes) to allow cancellation.

What happens during review
When the workflow reaches a job using a protected environment, it pauses and sends a notification to required reviewers. A reviewer visits the GitHub Actions run and clicks "Approve and deploy". Only then does the job continue.
Rollback: deploy a previous tag

Trigger a rollback by re-deploying a previous tag

Bash
# Find recent tags
git tag --sort=-version:refname | head -5
# v1.3.0
# v1.2.5
# v1.2.4
# v1.2.3
# v1.2.2

# Option A: use workflow_dispatch with version input
# (trigger the deploy workflow manually via GitHub UI, specifying the old version)

# Option B: re-tag to trigger deploy
git tag -f v1.2.5-rollback v1.2.5
git push origin v1.2.5-rollback

Workflow with manual rollback support

YAML
on:
  push:
    tags: ['v*.*.*']
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to deploy (e.g. v1.2.3)'
        required: true
        default: 'latest'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Determine version
        id: ver
        run: |
          if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
            echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
          else
            echo "VERSION=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT
          fi

      - uses: actions/checkout@v4
        with:
          ref: ${{ steps.ver.outputs.VERSION }}

      - run: npm ci && npm run build
      - run: ./deploy.sh ${{ steps.ver.outputs.VERSION }}
Blue-green deployments

Blue-green deployment runs two identical production environments. At any time, one is live ("blue") and one is idle ("green"). To deploy:

  1. Deploy new version to the idle environment (green).

  2. Run smoke tests against green.

  3. Switch the load balancer to point to green.

  4. Blue is now idle — keep it for instant rollback.

  5. On the next deploy, deploy to blue, test, switch.

  • Zero-downtime deployments — the switch is instant.

  • Instant rollback — flip the load balancer back if anything is wrong.

  • Requires double the infrastructure cost when both environments are running.

  • AWS Elastic Beanstalk, Kubernetes, and most PaaS platforms support this natively.

Feature flags as a deployment safety net

Feature flags decouple deployment from release. You deploy code to production with new features disabled, then gradually enable them — to 1% of users, then 10%, then all users. If something goes wrong, you disable the flag without redeploying.

  • Tools: LaunchDarkly, Unleash, PostHog feature flags, Growthbook.

  • Deploy continuously without fear — new code is off until you enable the flag.

  • A/B test features on a percentage of users.

  • Instantly disable a broken feature without a redeploy.

Tip
The simplest deployment safety net is a fast rollback. Know your rollback command before you deploy, test it in staging, and keep it in your team's runbook so any team member can execute it under pressure.