GitHub Actions
Gate a GitHub Actions workflow with a durable ActionBox decision and fail closed on rejection or expiry.
Use a GitHub runner to create an ActionBox Action, wait for the Source-scoped result, and run the deployment only when the stable option ID is approved. The workflow below calls the hosted API directly, so the workflow only needs its own shell steps and a Source key.
Public integration boundary
ActionBox currently documents the hosted REST pattern for GitHub Actions. A public Marketplace or release-tagged composite action is not part of the current customer distribution path. The shell workflow below uses the supported API contract and is suitable for a GitHub-hosted Ubuntu runner.
Before the workflow
- Create a Source in the ActionBox dashboard.
- Save its live key as the repository secret
ACTIONBOX_SOURCE_KEY. - Ensure the runner has
curlandjq. GitHub-hosted Ubuntu runners include both; install them explicitly on a self-managed runner.
Keep the Source key in GitHub's secret store. Never put it in workflow YAML, command arguments, logs, or Action context.
A fail-closed approval gate
This example creates one idempotent approval per run attempt, waits for up to
30 minutes, and runs the deployment only for the approve option. Rejection,
expiry, cancellation, timeout, and API errors cannot become an approval.
name: deploy-production
on:
workflow_dispatch:
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
env:
ACTIONBOX_API_URL: https://api.actionbox.cloud
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Request production approval
id: approval
timeout-minutes: 35
env:
ACTIONBOX_SOURCE_KEY: ${{ secrets.ACTIONBOX_SOURCE_KEY }}
ACTIONBOX_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}
shell: bash
run: |
set -euo pipefail
action_id=""
resolved="false"
cleanup() {
if [[ -n "$action_id" && "$resolved" != true ]]; then
curl --fail --silent --show-error \
--request POST \
"$ACTIONBOX_API_URL/v1/actions/$action_id/cancel" \
--header "Authorization: Bearer $ACTIONBOX_SOURCE_KEY" \
--header "Content-Type: application/json" \
--data '{"reason":"GitHub Actions stopped waiting"}' \
>/dev/null || true
fi
}
trap cleanup EXIT INT TERM
payload="$(jq -n \
--arg sha "$GITHUB_SHA" \
--arg workflow "$GITHUB_WORKFLOW" \
--arg repo "$GITHUB_REPOSITORY" \
--arg ref "$GITHUB_REF_NAME" \
--arg run "$ACTIONBOX_RUN_ID" \
--arg server "$GITHUB_SERVER_URL" \
'{
title: ("Approve " + $sha[0:7] + " for production?"),
description: "Required checks passed. Review the release before production changes.",
priority: "high",
dedupe_key: ("github-" + $run),
options: [
{id: "approve", label: "Approve deployment", style: "primary"},
{id: "reject", label: "Reject deployment", style: "destructive"}
],
metadata: {
origin: {
provider: "github_actions",
ref: ($workflow + " · run " + $run),
url: ($server + "/" + $repo + "/actions/runs/" + ($run | split("-")[0])),
labels: [$repo, $ref, $sha[0:7]]
}
}
}')"
created="$(curl --fail-with-body --silent --show-error \
--request POST "$ACTIONBOX_API_URL/v1/actions" \
--header "Authorization: Bearer $ACTIONBOX_SOURCE_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: github-$ACTIONBOX_RUN_ID" \
--data "$payload")"
action_id="$(jq -er '.data.id' <<<"$created")"
echo "action_id=$action_id" >>"$GITHUB_OUTPUT"
deadline=$(( $(date +%s) + 1800 ))
while (( $(date +%s) < deadline )); do
current="$(curl --fail-with-body --silent --show-error --max-time 45 \
"$ACTIONBOX_API_URL/v1/source/actions/$action_id?wait_seconds=30" \
--header "Authorization: Bearer $ACTIONBOX_SOURCE_KEY")"
status="$(jq -er '.data.status' <<<"$current")"
if [[ "$status" == open ]]; then
continue
fi
if [[ "$status" != resolved ]]; then
echo "Approval ended with status: $status" >&2
exit 1
fi
decision="$(jq -er '.data.resolution_option_id' <<<"$current")"
echo "decision=$decision" >>"$GITHUB_OUTPUT"
resolved="true"
exit 0
done
echo "No decision arrived within 30 minutes" >&2
exit 1
- name: Fail if the request was rejected
if: steps.approval.outputs.decision != 'approve'
run: |
echo "The production deployment was rejected."
exit 1
- name: Deploy to production
if: steps.approval.outputs.decision == 'approve'
run: ./scripts/deploy.shThe option IDs (approve and reject) are the machine contract. Labels can be
made more descriptive without changing the branch condition. The Source-scoped
read route supports a server wait of up to 30 seconds; it is not a decision and
the workflow continues polling while the Action remains open.
Record the execution result
An approval authorizes an attempt; it does not prove that the deployment
succeeded. After the deployment step, report success or failed to
POST /v1/actions/{action_id}/outcome with the resolved Action's
action_version and fingerprint. See Decisions and outcomes
for the exact binding and retry rules.
For a longer walkthrough with outcome reporting and the same hosted API contract, read the GitHub Actions deployment gate guide.
GitHub controls remain useful
GitHub Environments can restrict branches, protect environment secrets, and require repository reviewers. Keep those controls when they are part of your security model. ActionBox adds a shared decision inbox, bounded review context, and a durable decision record that other workers can consume.