Introduction
By day 40 of building your Git and GitHub workflow, you've mastered branching, code review, and merge strategies. Now comes the final—and most critical—phase: getting code to users safely. Deploying with GitHub Actions for Git & GitHub developers means automating your path from merge to production while maintaining control, auditability, and the ability to recover quickly when things go wrong.
This guide walks you through building a deployment pipeline that respects your team's safety guardrails: environment-specific protection rules, credential-free cloud access via OIDC, mandatory human sign-offs for production, and one-click rollback to any previous release.
Environments—Staging and Production with Protection Rules
GitHub Environments let you define per-environment secrets, variables, and deployment protection rules. They're your guardrails. Rather than storing AWS keys in your repository secrets (bad practice), you configure each environment separately: staging gets permissive rules, production gets strict ones.
Start by creating two environments in your repository settings: staging and production. For staging, you might allow any branch to deploy. For production, you restrict deployments to the main branch only and require an admin to approve.
Here's how to define this in your workflow file:
name: Deploy
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: |
echo "Deploying to staging..."
# Your deploy commands here
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
environment:
name: production
url: https://example.com
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: |
echo "Deploying to production..."
# Your deploy commands hereThe environment key ties each job to its environment. The url field displays the deployment link in your pull request—useful for QA testers. By setting if: github.ref == 'refs/heads/main', the production job only runs on main-branch pushes, not feature branches.
In the Settings > Environments panel, you can then configure:
- Deployment branches: Allow production deploys only from
mainor specific patterns. - Required reviewers: Enforce approval from at least one senior engineer before production deployment.
- Wait timer: Enforce a delay (useful for gradual rollout awareness).
Real-world example: A fintech SaaS team uses staging for every merge to main, giving product and QA a live preview within seconds. Production deploys happen hourly during business hours after a required reviewer sign-off, ensuring a senior engineer is always aware of what's live.
OIDC to Cloud—No Long-Lived Credentials
Storing AWS or cloud credentials in GitHub secrets is a liability. Every person with repository access can view them (if admin), they don't rotate, and if leaked, your infrastructure is exposed. GitHub's OpenID Connect (OIDC) integration solves this: Actions obtain short-lived, workload-specific credentials on-demand without ever storing long-lived keys in GitHub.
Set up OIDC trust between GitHub and your cloud provider. For AWS, create an Identity Provider in IAM pointing to GitHub's OIDC endpoint, then create a role that trusts GitHub Actions from your repository. Here's the AWS setup:
# In AWS CLI, create the identity provider
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
# Then create a role that trusts GitHub (via Terraform or CloudFormation for IaC)In your workflow, use the official AWS action with OIDC:
jobs:
deploy-production:
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write # Required for OIDC
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
aws-region: us-east-1
- name: Deploy to ECS
run: |
aws ecs update-service --cluster prod --service api --force-new-deploymentNo AWS keys anywhere. GitHub exchanges its JWT (signed proof of the workflow's identity) for temporary AWS credentials valid for one hour. Your team sleeps better, and compliance auditors are pleased.
This approach scales to GCP (via Workload Identity Federation), Azure (Federated Credentials), and other providers. Check your cloud provider's documentation for OIDC setup.
Manual Approvals—Required Reviewers for Production
Even the best automation can't catch every edge case. Required reviewers enforce a human checkpoint before production changes go live.
In GitHub's Environment settings, add specific users or teams as required reviewers. When a production deployment is triggered, the workflow pauses at the environment: production job and sends a notification to those reviewers. They review the deployment details—linked PR, commit message, what changed—and approve or reject in the UI.
Here's what a reviewer sees:
- Deployment summary (branch, commit SHA, changed files).
- Link to the triggering PR.
- Environment URL to verify staging before approving.
- Approve or Reject buttons.
Example workflow structure with built-in logging:
jobs:
build:
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.build.outputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
id: build
run: |
TAG="$GITHUB_SHA"
docker build -t myapp:$TAG .
docker push myapp:$TAG
echo "tag=$TAG" >> $GITHUB_OUTPUT
deploy-production:
needs: build
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- name: Log deployment
run: |
echo "Deploying image: ${{ needs.build.outputs.image-tag }}"
echo "Commit: ${{ github.sha }}"
echo "Triggered by: ${{ github.actor }}"
- name: Deploy
run: |
# Deployment commands
echo "Production deployment in progress..."A typical workflow: developer pushes to main, staging deployment runs automatically and passes integration tests. A Slack notification alerts the on-call engineer, who visits the staging URL to spot-check, then clicks "Approve" in GitHub's UI. Production deployment proceeds immediately.
Rollback—Redeploy a Previous Tag with One Click
Deployments fail. A bad merge slips through code review, or a third-party service behaves unexpectedly. You need to revert to the last known good state fast, without waiting for a new commit or PR cycle.
Tag every production release. When you deploy version v1.2.3, create a Git tag. Then, use a workflow dispatch input to allow manual rollbacks:
name: Rollback Production
on:
workflow_dispatch:
inputs:
tag:
description: 'Git tag to rollback to'
required: true
default: 'v1.2.2'
jobs:
rollback:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag }}
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
aws-region: us-east-1
- name: Deploy rolled-back version
run: |
docker pull myapp:${{ github.event.inputs.tag }}
aws ecs update-service \
--cluster prod \
--service api \
--force-new-deployment \
--task-definition myapp:${{ github.event.inputs.tag }}When things go wrong, a senior engineer opens the "Rollback Production" workflow in GitHub's Actions tab, enters v1.2.2 in the tag input, clicks "Run workflow," and production is back to the previous version in under a minute. No SSH, no manual scripts, full auditability.
Pro tip: Tag releases from your CI/CD pipeline automatically. When a production deployment succeeds, have the workflow create and push a tag:
- name: Tag release
run: |
git config user.email "actions@github.com"
git config user.name "GitHub Actions"
git tag -a "prod-$(date +%Y%m%d-%H%M%S)" -m "Production deployment"
git push origin --tagsCommon Mistakes
- Storing long-lived credentials: Using AWS keys in repository secrets instead of OIDC. Credentials get exposed, leaked, or rotated inconsistently.
- Skipping environment protection: Deploying production from any branch without restrictions. A feature branch accidentally merged and deployed before review.
- No approval trail: Automatic production deployments with zero human oversight. Compliance and incident response become nightmares.
- Forgetting to tag releases: Treating commit SHAs as version identifiers. When rollback is needed, you hunt through logs to find the right version.
- Ignoring workflow logs: Not capturing what was deployed (commit, tag, actor) in deployment logs. Incident investigation becomes slow and painful.
- Missing rollback tests: Assuming rollback will work without rehearsal. Test it in staging first; validate that redeploying an old tag succeeds.
Key Takeaways
- Define separate environments (staging, production) with GitHub's built-in Environment settings and apply branch-specific protection rules to each.
- Use OIDC for cloud credentials instead of long-lived keys: rotate automatically, audit by workload, and never store secrets in the repository.
- Enforce required reviewers on production deployments to ensure a human sanity-check before live code reaches users.
- Tag all production releases and create a manual rollback workflow so recovery from bad deployments takes minutes, not hours.
- Log deployment metadata (commit, tag, actor, timestamp) and link back to PRs so your team has a complete audit trail for incident response.
- Test your rollback process in staging before you need it in production—assumptions about rollback success often fail under pressure.






Comments
Loading comments…