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:
- Runs the full test matrix and image build automatically
- Creates an Actionbox deployment approval with context (env, commit, diff summary)
- Blocks until a human approves or rejects from the dashboard, phone, or CLI
- Deploys only on
approve; cancels and pages onrejector 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.
- 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:
- 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 1Rejections 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:
- 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
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 1Why 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_expiredor an explicit fallback response when nobody answers
Try it
The gate is two files away from live:
- Create a free Source in the dashboard →
ACTIONBOX_TOKEN - Add the
actionbox/approvalstep to your workflow - Deploy a branch and watch the pipeline pause at the gate