GitHub Actions Manual Approval: Approve Deploys From Your Phone
Published:2026-11-08
The built-in way to add manual approval to GitHub Actions is an environment with required reviewers. On GitHub Free, Pro, and Team plans, required reviewers work only in public repositories. For private repositories, use a step that waits instead, such as an issue-based approval action or blipdrop/nodd-action, which sends the approval to an iPhone.
Why pause a deploy for a person
Production deploys, database migrations, and releases are things you want automated, but you still want a person to look before they run. Even a reviewed change on main may be wrong to ship right now because of the time of day or other work in flight.
Teams usually want three things:
- Stop the workflow right before the deploy and continue only when someone approves
- Decide from a phone, without sitting at a computer
- Keep the reason when someone rejects
Environments with required reviewers
This is GitHub's built-in option. Create an environment such as production in the repository settings and add required reviewers to its protection rules. A job that sets environment pauses before it starts and waits for approval.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
According to GitHub's documentation:
- You can list up to six users or teams as reviewers. They need at least read access to the repository
- Only one of the listed reviewers needs to approve
- You can prevent the person who started the deployment from approving it
- Reviewers can leave a comment when they approve or reject. Rejecting fails the workflow
- A waiting job shows as Waiting and fails automatically if it is not approved within 30 days
- A wait timer can delay the job by 1 to 43,200 minutes (30 days)
- The job cannot access the environment's secrets until it is approved
GitHub Mobile can send push notifications for deployment review requests and lets reviewers approve or reject them (announced in GitHub's changelog in 2021).
The strengths are real: the job stops before it starts, and approval is tied to who can use the environment's secrets. The rules live in settings, outside the workflow file, which makes them easy to audit.
The limit is availability. On GitHub Free, Pro, and Team plans, required reviewers and wait timers are available only for public repositories. Private repositories need GitHub Enterprise. Also, a rejected job never starts, so that job cannot read and act on the reviewer's comment.
Issue-based approval actions
Community actions fill the gap for private repositories. The widely used trstringer/manual-approval opens an issue during the run and assigns it to approvers. A comment such as approve or lgtm lets the run continue, and deny fails it.
- uses: trstringer/manual-approval@v1
with:
secret: ${{ github.TOKEN }}
approvers: user1,user2
minimum-approvals: 1
issue-title: "Deploying v1.3.5 to prod"
- Works in private repositories at no extra cost
- Supports a minimum number of approvals and excluding the person who started the run
- Returns the result in the
approval-statusoutput (approvedordenied) - Because it is an issue comment, approvers can answer from the GitHub mobile app
The trade-off is that the step keeps running on a runner while it waits, so it uses runner time. Its README notes that the wait is subject to the workflow time limit and usage costs. Approval is decided by the words in a comment, so identity rests on the GitHub session.
Comparison
| Compared | Environments / Issue-based action / Nodd GitHub Action |
|---|---|
| Where you approve | GitHub web, GitHub Mobile / A GitHub issue / The Nodd app on an iPhone |
| Private repos on Free, Pro, Team | No (needs Enterprise) / Yes / Yes |
| Identity check on each response | GitHub session / GitHub session / Face ID every time |
| Where it waits | Before the job starts (no runner) / Inside a step / Inside a step |
| Rejection reason in later steps | No (the job never starts) / Result value only / Yes, the note output |
| How long it waits | Fails after 30 days / Up to job and workflow limits / Until 15 minutes after expires-in |
| Two or more approvers | No (one approval is enough) / minimum-approvals / CLI --approvals (Team plan) |
| Protects secrets | Withholds environment secrets until approved / No / No |
If your repository is public or you are on GitHub Enterprise, start with environment protection rules. If the repository is private and your approvers live in GitHub, an issue-based action is enough. The next option fits when you want to decide from a notification without opening GitHub, require Face ID for the answer, or get the rejection reason back into the workflow.
Approve on an iPhone with the Nodd GitHub Action
Nodd is an iPhone app that receives questions from scripts and CI and returns your Face ID response to the program that asked. blipdrop/nodd-action@v1 waits for approval inside a step.
- Create a channel in the Nodd app and issue a send key
- Add it to your repository secrets as
NODD_KEY - Add the step to your workflow. The runner needs
curlandjq
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- id: approval
uses: blipdrop/nodd-action@v1
with:
key: ${{ secrets.NODD_KEY }}
title: Deploy to production?
body: ${{ github.sha }}
level: high
wait: "true"
expires-in: "1800"
- name: Show the rejection reason
if: failure() && steps.approval.outputs.decision == 'rejected'
env:
NOTE: ${{ steps.approval.outputs.note }}
run: echo "$NOTE"
- run: ./deploy.sh
With wait: "true", the step succeeds only when an approval is recorded. Rejection, expiry, cancellation, and failure all fail the job, so ./deploy.sh runs only after approval. If the responder writes a reason, it comes back in the note output. note is text a person wrote, so pass it through env as shown instead of putting it directly in run.
Inputs and outputs
key | Required. Send key, passed from secrets and masked in logs |
|---|---|
title | Required. Notification title |
body | Body |
level | low, normal, or high. Default normal |
wait | "true" asks for approval and waits. Default "false" |
expires-in | Response deadline in seconds. Default "3600" |
api-url | API URL |
Output id | Message ID |
Output decision | approved, rejected, expired, cancelled, or failed. Set only with wait: "true" |
Output note | The reason the responder wrote, if any. Line breaks become spaces |
Without wait, the same action just sends a notification, for example when a deploy finishes or fails.
Use the CLI in a workflow step
The GitHub Action has no inputs for multiple approvers or escalation. When you need them, use nodd ask from the CLI. In CI, skip login and pass the send key in NODD_KEY. Binaries are on Releases (the version below is the one current when this was written).
- name: Install nodd
run: |
curl -sSL https://github.com/blipdrop/nodd/releases/download/v0.3.0/nodd-x86_64-unknown-linux-musl.tar.gz \
| tar xz -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Approve the production DB change
env:
NODD_KEY: ${{ secrets.NODD_KEY }}
run: |
nodd ask "Change the production database?" --level high \
--field Environment=production \
--approvals 2 --escalate-after 600 --expires-in 1800
nodd ask exits 0 on approval, 10 on rejection, and 11 when it ends without a response, such as on expiry, so the step succeeds only on approval. A rejection reason goes to standard error as Note: reason. More than one approver with --approvals, and --escalate-after, need the Team plan.
Things to watch
- The Nodd GitHub Action waits until 15 minutes after
expires-in. Set the job'stimeout-minuteslonger than that. GitHub's default jobtimeout-minutesis 360, and jobs on GitHub-hosted runners can run for up to 6 hours - Because the wait happens inside a step, it uses runner time. It is not a good fit for very long deadlines
- Anyone who can edit the workflow can remove an in-step approval. Keep production secrets in an environment and combine this with GitHub's permission settings
- Nodd's Free plan is one member and one device, with up to 3 open questions. Use a Team plan when a team shares approvals
- Never treat no response as approval. On expiry the job fails
FAQ
- Can I use environment required reviewers in a private repository?
- On GitHub Free, Pro, and Team plans, required reviewers and wait timers are available only for public repositories. Private repositories need GitHub Enterprise. Otherwise, use an action that waits for approval inside a step.
- Can I approve a deployment from GitHub Mobile?
- Yes. Required reviewers on an environment get push notifications in GitHub Mobile and can approve or reject from the app.
- Can the workflow read the reason for a rejection?
- With the Nodd GitHub Action, the reason the responder wrote is in the note output. The CLI prints it to standard error as Note. A job rejected through environments never starts, so that job cannot read the comment.
- What happens if nobody responds?
- With environments, a job that is not approved within 30 days fails automatically. With Nodd, the message becomes expired after its deadline and the job fails. Neither continues without approval.
- Can Nodd require two approvers?
- Add --approvals 2 to nodd ask in the CLI and it waits until two people approve. More than one approver needs the Team plan, and the GitHub Action has no input for it.
Nodd delivers questions from scripts, CI, and AI agents to your iPhone, and returns your Face ID approval to the program that asked. There is a free plan.