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
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
# 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
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: trueDeploy to AWS S3 + CloudFront
.github/workflows/deploy-aws.yml
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
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 }}Environment protection rules
Go to Settings → Environments → New environment → name it "production".
Enable "Required reviewers" and add the names of who can approve deploys.
Set "Deployment branches" to "Selected branches" → main only.
Add any environment-specific secrets (production API keys, etc.).
Optional: enable "Wait timer" (e.g. 10 minutes) to allow cancellation.
Rollback: deploy a previous tag
Trigger a rollback by re-deploying a previous tag
# 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
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:
Deploy new version to the idle environment (green).
Run smoke tests against green.
Switch the load balancer to point to green.
Blue is now idle — keep it for instant rollback.
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.