ActionBoxBlog

DevOpsPlatformBackend

GitHub Actions production deployment approval gate

A copy-paste GitHub Actions production deployment approval workflow with exact YAML, an SLA countdown, fail-closed behavior, and an audit trail.

Your CI pipeline runs tests, lints, scans — and then deploys to production with zero human judgment. One bad commit and nobody sees it until support tickets arrive. An approval gate inserts a human decision between "pipeline is green" and "production changed". It's the difference between automation that ships your mistakes and automation that ships your intent.

What you'll build

A GitHub Actions workflow where the deploy-production job:

  1. Runs the full test matrix and image build automatically
  2. Creates an Actionbox deployment approval with context (env, commit, diff summary)
  3. Blocks until a human approves or rejects from the dashboard, phone, or CLI
  4. Deploys only on approve; cancels and pages on reject or timeout

The whole gate is ~30 lines of YAML on top of your existing workflow.

Step 1: the approval step

The Actionbox CLI creates the Action, records provenance from the runner environment, and long-polls until the decision lands — no polling loop or webhook endpoint is required in the workflow.

yaml
- name: Install Actionbox CLI
  run: curl -fsSL https://actionbox.cloud/install.sh | sh

- name: Request deployment approval
  id: deploy-approval
  env:
    ACTIONBOX_TOKEN: ${{ secrets.ACTIONBOX_TOKEN }}
  run: |
    result=$(actionbox ask "Deploy $GITHUB_SHA to production?" \
      --option approve="Approve deploy" \
      --option reject="Reject deploy" \
      --context-json "[{\"type\":\"key_value\",\"items\":{\"environment\":\"production\",\"commit\":\"$GITHUB_SHA\",\"workflow\":\"$GITHUB_WORKFLOW\"}}]" \
      --wait --timeout 30m --json)
    echo "decision=$(jq -r '.decision // empty' <<< "$result")" >> "$GITHUB_OUTPUT"

Set timeout to how long you're willing to hold the runner for a decision — 30 minutes for a normal deploy, longer for a compliance release. When the Action expires unanswered, the workflow gets the expired status and your on-expire-json fallback runs instead of a silent pass.

Step 2: branch on the decision

The Action exposes decision and response-json outputs. Use if conditions to deploy only on approval:

yaml
- name: Deploy to production
  if: steps.deploy-approval.outputs.decision == 'approve'
  run: ./scripts/deploy.sh

- name: Cancel pipeline
  if: steps.deploy-approval.outputs.decision != 'approve'
  run: |
    echo "Deployment was not approved (decision: ${{ steps.deploy-approval.outputs.decision }})"
    exit 1

Rejections and timeouts now fail the workflow with a clear reason instead of silently doing nothing. Your Slack/email notifiers pick up the failure path automatically.

Step 3: reject means page, don't just log

A rejected deploy is an incident, not a log line. Chain the failure path to your paging channel:

yaml
- name: Page on rejection
  if: steps.deploy-approval.outputs.decision == 'reject'
  run: |
    curl -fsS -X POST https://api.pagerduty.com/v2/enqueue \
      -H "Authorization: Token token=${{ secrets.PAGERDUTY_TOKEN }}" \
      -H "Content-Type: application/json" \
      -d '{"routing_key": "${{ secrets.PAGERDUTY_ROUTING_KEY }}", "event_action": "trigger", "payload": {"summary": "Production deploy rejected", "source": "actionbox-deploy", "severity": "critical"}}'

Full workflow

yaml
name: deploy-production
on:
  push:
    branches: [main]

permissions:
  contents: read

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

      - name: Run tests
        run: npm ci && npm test

       - name: Install Actionbox CLI
         run: curl -fsSL https://actionbox.cloud/install.sh | sh

       - name: Request deployment approval
         id: approval
         env:
           ACTIONBOX_TOKEN: ${{ secrets.ACTIONBOX_TOKEN }}
         run: |
           result=$(actionbox ask "Deploy $GITHUB_SHA to production?" \
             --option approve="Approve deploy" \
             --option reject="Reject deploy" \
             --wait --timeout 30m --json)
           echo "decision=$(jq -r '.decision // empty' <<< "$result")" >> "$GITHUB_OUTPUT"

      - name: Deploy
        if: steps.approval.outputs.decision == 'approve'
        run: ./scripts/deploy.sh

      - name: Fail on reject or timeout
        if: steps.approval.outputs.decision != 'approve'
        run: |
          echo "Deploy not approved: ${{ steps.approval.outputs.decision }}"
          exit 1

Why this beats "environment protection rules" alone

GitHub's built-in environment reviewers work, but they're inside GitHub: a human gets an email, opens the UI, clicks approve. Context is thin, the queue is per-repository, and there's no record beyond the audit log. Actionbox keeps the decision in your pipeline:

  • Rich context — commit, environment, and workflow data render in the dashboard before the buttons
  • Typed decisions — boolean, single/multi choice, numeric, or free-text responses feed straight into your automation
  • Same pattern anywhere — the identical gate works in GitLab, Jenkins, CircleCI, cron, and scripts (see Integrate From Anywhere)
  • Expiry behavior — choose return_expired or an explicit fallback response when nobody answers

Try it

The gate is two files away from live:

  1. Create a free Source in the dashboard → ACTIONBOX_TOKEN
  2. Add the actionbox/approval step to your workflow
  3. Deploy a branch and watch the pipeline pause at the gate

Create a free Source · Full API reference · CLI guide

Try this workflow in minutes

Create a free Source, then run the exact commands from this post against the live API — no approval infrastructure to build.

S
Suson Sapkota

Founder, Actionbox