GitHub Actions Approval Gates:
Human-in-the-Loop for CI/CD Deployments

GitHub's built-in environment protection rules help, but they only gate the entire job — not individual commands. Here's how to add per-command human approval to your deployment workflows.

GitHub Actions has environment protection rules that require a reviewer to approve a deployment job before it runs. Useful — but blunt. Once approved, the job runs every command without further checks.

If your deployment workflow runs:

- name: Deploy to production
  run: |
    npm run build
    aws s3 sync dist/ s3://my-prod-bucket --delete
    aws cloudfront create-invalidation --distribution-id EXXXXX --paths "/*"
    kubectl set image deployment/api api=myapp:${{ github.sha }}
    kubectl rollout status deployment/api

One "approve" covers all of it. The reviewer said "deploy" — they didn't necessarily think through the CloudFront cache invalidation timing or the kubectl rollout touching all pods during peak traffic.

With expacti, you gate each command individually. The workflow pauses before each risky step and waits for explicit reviewer approval — with full context of what the command does, its risk score, and what ran before it.

How it works

Workflow step executes

The expacti-action composite action intercepts the command before it runs and sends it to the expacti backend for review.

Reviewer is notified

Your designated reviewer gets a Slack message, email, or browser push with the exact command, risk score, and workflow context.

Reviewer approves or denies

One click in the reviewer dashboard (or directly in Slack), and the command proceeds or the workflow step fails fast.

Immutable audit trail created

Every decision is logged: timestamp, reviewer identity, exact command, review latency. Exportable for SOC 2 and ISO 27001.

Setup in 5 minutes

Step 1: Configure secrets

Add these as GitHub Actions secrets in your repository settings:

Step 2: Replace risky steps with the composite action

name: Deploy to Production

on:
  push:
    branches: [main]

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

      - name: Build
        run: npm ci && npm run build
        # Build steps are deterministic — no approval needed

      - name: Sync to S3
        uses: kwha/expacti-action@v1
        with:
          command: aws s3 sync dist/ s3://my-prod-bucket --delete
          expacti-url: ${{ secrets.EXPACTI_URL }}
          shell-token: ${{ secrets.EXPACTI_SHELL_TOKEN }}
          timeout: "300"

      - name: Invalidate CDN
        uses: kwha/expacti-action@v1
        with:
          command: aws cloudfront create-invalidation --distribution-id EXXXXX --paths "/*"
          expacti-url: ${{ secrets.EXPACTI_URL }}
          shell-token: ${{ secrets.EXPACTI_SHELL_TOKEN }}

      - name: Rollout new image
        uses: kwha/expacti-action@v1
        with:
          command: kubectl set image deployment/api api=myapp:${{ github.sha }}
          expacti-url: ${{ secrets.EXPACTI_URL }}
          shell-token: ${{ secrets.EXPACTI_SHELL_TOKEN }}

Each step using the expacti action pauses the workflow until a reviewer approves it. If nobody approves within the timeout, the step fails and the workflow stops — safely.

Common patterns

Database migration gate

- name: Run migrations
  uses: kwha/expacti-action@v1
  with:
    command: npm run db:migrate -- --env production
    expacti-url: ${{ secrets.EXPACTI_URL }}
    shell-token: ${{ secrets.EXPACTI_SHELL_TOKEN }}
    timeout: "600"

Migrations are irreversible. Even a small schema change can cause downtime at the wrong moment. A reviewer confirming "run this now" is worth the 30-second interruption.

Production-only gates

- name: Deploy (production — gated)
  if: github.ref == 'refs/heads/main'
  uses: kwha/expacti-action@v1
  with:
    command: ./deploy.sh production
    expacti-url: ${{ secrets.EXPACTI_URL }}
    shell-token: ${{ secrets.EXPACTI_SHELL_TOKEN }}

- name: Deploy (staging — automatic)
  if: github.ref != 'refs/heads/main'
  run: ./deploy.sh staging

Staging deployments run automatically. Production requires a human. Same workflow, branch-conditional behavior.

Progressive regional rollout

jobs:
  deploy:
    strategy:
      matrix:
        region: [us-east-1, eu-west-1, ap-southeast-1]
      max-parallel: 1  # serialize across regions
    steps:
      - name: Deploy ${{ matrix.region }}
        uses: kwha/expacti-action@v1
        with:
          command: ./deploy.sh ${{ matrix.region }}
          expacti-url: ${{ secrets.EXPACTI_URL }}
          shell-token: ${{ secrets.EXPACTI_SHELL_TOKEN }}

With max-parallel: 1, the reviewer approves US first, watches for errors, then approves EU, then APAC. Classic progressive rollout with a human checkpoint between regions.

Handling off-hours deployments

Deployments don't always happen at 2pm on a Tuesday. Expacti handles this:

⚠ Timeout behavior

By default, commands that time out are denied, not auto-approved. If you want low-risk commands (like git pull) to auto-approve on timeout, configure a whitelist rule in expacti — the CI step doesn't need to change.

How this compares to GitHub's built-in protection

GitHub's environment protection rules already support required reviewers. So why add expacti?

They're complementary. GitHub protection is a job-level gate; expacti is a command-level gate. Use both for defense in depth.

The whitelist: how repetitive commands become automatic

You don't want a reviewer approving npm ci on every commit. Expacti's whitelist handles this: commands matching established rules run without interruption. Only novel or higher-risk commands pause for review.

On your first few deployments, more steps will need approval. Over time, your whitelist grows based on observed patterns. The AI suggestion engine also proposes glob patterns — turning 20 individual kubectl set image deployment/api api=myapp:XYZ approvals into one approved glob rule.

💡 Start tight, loosen with data

Begin with no whitelist rules — every command requires approval for the first 2–3 deployment cycles. Then accept the AI-suggested patterns. You'll end up with a precise, evidence-based whitelist instead of a guessed one.

Getting started

  1. Sign up at expacti.com and get your reviewer account
  2. Generate a shell token from the dashboard
  3. Add EXPACTI_URL and EXPACTI_SHELL_TOKEN to your repository secrets
  4. Replace one risky deployment step with kwha/expacti-action@v1
  5. Run the workflow and approve the command in the dashboard

The full GitHub Actions integration guide covers advanced patterns: multi-environment flows, approval routing by risk score, and OpsGenie/PagerDuty integration for on-call routing.

Gate your deployments, not just your jobs

Add expacti to one GitHub Actions workflow today. Five minutes to set up, command-level audit trails from day one.

Get early access See the demo