Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 040 / 100

Deploying with GitHub Actions for Git & GitHub developers

From merge to production.

Expona Admin7 min read

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:

bash
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 here

The 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 main or 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:

bash
# 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:

bash
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-deployment

No 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:

bash
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:

bash
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:

bash
      - 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 --tags

Common 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.
---
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles