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:
EXPACTI_URL— your expacti backend WebSocket URL (e.g.,wss://your-instance.expacti.com)EXPACTI_SHELL_TOKEN— a shell token from the expacti dashboard
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:
- Email escalation — if the primary reviewer doesn't respond in N minutes, the backup reviewer is notified
- Slack with multiple viewers — anyone in the channel can approve, not just one designated person
- PagerDuty/OpsGenie webhook — route high-risk deployment commands to your on-call rotation
- Auto-deny on timeout — if nobody responds, the step fails safely rather than proceeding without oversight
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?
- Granularity — GitHub gates the entire job. Expacti gates individual commands within a job.
- Risk scoring — the reviewer sees why a command is flagged, not just "please approve this job"
- Richer audit trail — which specific command, at what time, by whom, with review latency. GitHub logs "deployment approved" — a much coarser record.
- Whitelist memory — routine deployments auto-approve once established; novel or higher-risk commands still get human review
- Works outside CI too — same approval flow for AI agents, cron scripts, and manual SSH sessions
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.
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
- Sign up at expacti.com and get your reviewer account
- Generate a shell token from the dashboard
- Add
EXPACTI_URLandEXPACTI_SHELL_TOKENto your repository secrets - Replace one risky deployment step with
kwha/expacti-action@v1 - 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.