GitHub
A pull request that’s ready gets merged when the keys turn, a colleague’s pull request gets your approving review, and a production deployment waiting on a protection rule gets its answer. Requests come from the App’s webhooks, or from a workflow that asks and polls.
Actions
Section titled “Actions”Every organization is seeded with these three when it’s created and when it connects the App. An admin can change any field on the dashboard.
| Action | Keys | Mode | Request TTL | Arm window | Code TTL | Self approval |
|---|---|---|---|---|---|---|
github.merge-own-pr |
1 | counted | 30m | 60s | 15m | allowed |
github.approve-pr |
1 | counted | 4h | 60s | 15m | denied |
deploy.production |
2 | coincident | 30m | 20s | 15m | denied |
A production deploy is two keys inside one twenty second window. With the requester struck out, that takes three eligible people.
deploy.production carries what GitHub sends: repo, env and sha, plus ref, run, deployment and installation. The App fills in the last three, and they’re how the answer finds its way back to the paused run.
Set up the App
Section titled “Set up the App”Outis runs one GitHub App, and your organization installs it. Press Connect on your organization’s GitHub screen, pick your account and the repositories, and GitHub sends you back. Then, for each environment whose deployments should wait for keys, open the environment’s settings in GitHub and add the Outis App under Deployment protection rules.
The App asks for this and nothing more:
| Permission | Access | Why |
|---|---|---|
| Actions | read | GitHub requires it of a custom deployment protection rule. |
| Checks | read | Tells the App when a pull request’s checks pass. |
| Contents | write | Merging a pull request writes to its base branch. |
| Deployments | write | Approving or rejecting a waiting deployment. |
| Metadata | read | Every App gets it. |
| Pull requests | write | Reading pull requests, leaving the review, and commenting when nothing was merged. |
It subscribes to four events: deployment_protection_rule, pull_request, pull_request_review and check_suite.
If you run Outis yourself, the App’s manifest ships with the GitHub integration as app-manifest.json, with those permissions and events. Register it on GitHub (swap in your own hosts), then set the App’s id, slug, webhook secret and private key in the server’s environment. The integration’s README has the steps.
From the App’s webhooks
Section titled “From the App’s webhooks”Four events are read; everything else is acknowledged and ignored.
| Event | What it proposes |
|---|---|
deployment_protection_rule (requested) |
deploy.production, once per deployment. The workflow run pauses until the keys turn. |
pull_request (opened, synchronize, ready_for_review, edited, reopened), pull_request_review (submitted), check_suite (completed, success) |
github.merge-own-pr, once per head sha, when GitHub reports mergeable_state as clean. |
pull_request (review_requested) naming an operator |
github.approve-pr, once per head sha. |
The requester is matched by GitHub login to a member of your organization. For a pull request that’s the author. For a deployment it’s whoever created it, except when that’s a bot: a workflow’s deployments are created by github-actions[bot], so the person who triggered the run is the requester, and the request’s summary names both.
How long a deployment waits
Section titled “How long a deployment waits”GitHub holds a run on a protection rule for up to 30 days, but the request itself lives as long as the action’s request TTL, 30 minutes by default. When it ends without the keys, the run is rejected.
A deployment that can’t become a request is rejected right away, with the reason in the rule’s comment:
- The requester’s GitHub login isn’t a member of your organization.
- Outis refused the request, say no policy allows it or there aren’t enough eligible people to make the quorum.
Fix the cause, then re-run the job.
Re-runs and redelivery
Section titled “Re-runs and redelivery”Re-running a job sends a fresh event, and it’s a new request with its own keys. GitHub creates a new deployment for each attempt, and the deployment is what Outis keys on. For an environment that creates no deployment, the key is the run, the environment and the commit. GitHub doesn’t say which attempt an event is for, so there a re-run lands on the first attempt’s request. Start a new run to ask again.
A redelivery is the same event sent again, from the App’s Advanced settings or after a failed delivery. It lands on the request the first one made, so nobody’s asked twice.
What operators see
Section titled “What operators see”The panel for a deploy reads like this:
GITHUB DEPLOY
payments-api
ENV production
SHA 8d93f71
That’s the repository, the environment, and the first seven characters of the commit, which is how GitHub abbreviates it. Anything that looks like production (production, prod, or a name with either as a word, like eu-production) is drawn at critical severity. The core adds the quorum line underneath, like 2 OF 3.
A merge draws MERGE PR over the pull request number, and an approval draws APPROVE PR, in a different color so the two can’t be confused. Both carry a DGST field, the same six characters the notification prints, so the operator can check they match before pressing.
What GitHub hears back
Section titled “What GitHub hears back”Once the request is decided, the App makes one call with its installation token: the merge at the sha the operators saw, the review, or the protection rule’s answer for the run and environment the panel showed. A head that moved since then isn’t merged.
The comment on the answer says who approved it (by their Outis operator ids) or how it ended otherwise. Denied, expired and withdrawn each read differently. It also links the request on your dashboard and gives its operation hash, all inside GitHub’s 1024 character limit.
If GitHub refuses the answer (the run’s gone, it was already answered, the head moved), the request lands in failed, where your operators see it. If GitHub’s just unreachable, Outis tries again until it lands.
From a workflow
Section titled “From a workflow”A workflow that doesn’t use the App, or that’s asking for something GitHub has no event for, creates a request and polls with an API key holding propose and read. The App doesn’t act on a request made this way, even if its params name an installation, so your workflow reads the decision and does the work itself. The idempotency key below is the run and its attempt, so a retried step lands on the same request and a re-run makes a new one.
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Ask for the keys, then wait for the answer
env:
OUTIS_KEY: ${{ secrets.OUTIS_KEY }}
run: |
ID=$(curl -sS -X POST "https://api.outis.tech/v1/requests" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OUTIS_KEY" \
-H "Idempotency-Key: $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
-d "{\"action\":\"deploy.production\",\"requester\":\"$GITHUB_ACTOR\",
\"params\":{\"repo\":\"$GITHUB_REPOSITORY\",\"env\":\"production\",\"sha\":\"$GITHUB_SHA\"}}" \
| jq -r .request.id)
until [ "$(curl -sS "https://api.outis.tech/v1/requests/$ID" -H "Authorization: Bearer $OUTIS_KEY" | jq -r .request.outcome)" != "null" ]; do
sleep 5
done
test "$(curl -sS "https://api.outis.tech/v1/requests/$ID" -H "Authorization: Bearer $OUTIS_KEY" | jq -r .request.outcome)" = authorized
- name: Deploy
run: ./deploy.sh
The GitHub integration also has its own route, POST /integrations/github/propose, behind the same kind of key. It takes the same body for its three actions and answers with request_id and the digest.
Signatures
Section titled “Signatures”Webhooks are signed by GitHub with the App’s secret and checked on the webhook route: X-Hub-Signature-256 over the raw body, compared in constant time. A repeated delivery is caught by the request’s idempotency key, which Outis keeps for your organization, so a redelivery never becomes a second request. The request routes read no signature header; your API key is the credential.