Open-source project
trstringer/manual-approval avatar
trstringer/manual-approval

manual-approval pauses a GitHub Actions run until a human replies

Pause your GitHub Actions workflow and request manual approval from set approvers before continuing

678 stars158 forksGoMIT

At a glance

What is it?
manual-approval is a Go GitHub Action that opens an issue, assigns it to a set of approvers and blocks a workflow until they approve or deny it in a comment. It exists because GitHub's own deployment review needs environments, and environments need GitHub Enterprise for private repositories, so this action gives private repos the same gate for free.
Who is it for?
manual-approval is worth adopting for a team on GitHub whose releases or deployments need a human checkpoint and whose repositories are private, since it delivers that gate without the environment configuration GitHub's own review feature requires. It is the wrong choice if you already have environments available and configured, since the native path needs no extra action and no polling, and it is the wrong choice for a Windows runner, which it does not support.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 47 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why an action exists when GitHub already reviews deployments

The readme explains the motivation in the first paragraph and the reasoning is worth repeating because it defines who this is for. Manual approval before a deployment or release is a common pipeline feature, and GitHub does offer it through reviewing deployments. The catch the readme names is that GitHub's functionality requires the use of environments, and to use it for private repositories you need GitHub Enterprise. The action therefore provides manual approval without environments and is freely available on private repositories. That is a narrow, defensible gap, and it means the audience is teams whose repositories are private and whose plans do not include enterprise features, or teams that would rather not configure environment protection rules for every deployment path. The mechanism it substitutes is not a native review UI, it is an issue with a comment thread, which is worth keeping in mind, because that choice determines most of the behaviour below, including the latency and the API cost.

The approval flow is an issue, a comment thread and a poll loop

The action's behaviour is described in four steps. The workflow reaches the manual-approval action, the action creates an issue in the containing repository and assigns it to the approvers, and if all approvers respond with an approved keyword the workflow continues. If any approver responds with a denied keyword the workflow exits with a failed status. The keyword lists are short. Approval words are approve, approved, lgtm and yes, denial words are deny, denied and no. They are case insensitive and tolerate an optional period or exclamation mark, which covers the difference between how people type lgtm and LGTM. and lgtm! without any configuration. In all cases the action closes the initial issue, so the thread does not stay open as a permanent artefact after a decision. The mechanics sit in Go source files at the repository root, with approval.go, approvalstatus.go, approvers.go and constants.go alongside main.go, and tests next to them in approval_test.go and approvers_test.go. The go.mod pins the GitHub API client at github.com/google/go-github/v43 v43.0.0 with golang.org/x/oauth2 v0.33.0 for authentication, and requires Go 1.25.0.

Configuring the gate: approvers, minimum approvals and self-approval control

The action is wired into a workflow with a list of inputs, and the documented example is the reference for what each one does:

yaml
steps:
  - uses: trstringer/manual-approval@v1
    with:
      secret: ${{ github.TOKEN }}
      approvers: user1,user2,org-team1
      minimum-approvals: 1
      issue-title: "Deploying v1.3.5 to prod from staging"
      issue-body: "Please approve or deny the deployment of version v1.3.5."
      issue-body-file-path: relative/file_path/wrt/repo/root
      exclude-workflow-initiator-as-approver: false
      fail-on-denial: true
      additional-approved-words: ''
      additional-denied-words: ''
      polling-interval-seconds: 10

The approvers list is comma delimited and each entry is either a user or an org team. There is a permission requirement that is easy to miss, and the readme is explicit that required approvers must have the ability to be set as approvers in the repository, and that adding one without that permission produces an HTTP/402 Validation Failed error when the action runs. minimum-approvals sets how many approvals are needed to continue and defaults to all approvers, so the default behaviour is unanimous. exclude-workflow-initiator-as-approver filters the person who triggered the workflow, determined by the GITHUB_ACTOR environment variable, out of the approver list, and setting it to true is how you stop the person who opened the pull request from approving their own release. fail-on-denial decides whether a denial fails the workflow and defaults to true. The two word lists extend the built in vocabulary, and the polling interval defaults to 10 seconds, which the readme says you can increase to reduce API calls or decrease for faster response.

Issue bodies have a size limit and the chunking can trip a rate limit

Two notes in the readme describe a real operational failure mode, and they are worth reading before you wire this into a pipeline with a large changelog. The first is that when using issue-body, the content string is passed as an argument and GitHub limits that to 10kb, so for content of 10kb or more you are told to pass a file instead. The second is a caution about file size. The safe limit is under 125 KB, and the readme explains why with a specific error. If the file is huge, its content is broken into multiple chunks, each representing an issue comment, and with that many API requests the API rate limit is exceeded and the action is temporarily blocked, with an error like 403 You have exceeded a secondary rate limit and have been temporarily blocked from content creation. The explanation that follows matters for planning. Secondary limits apply to a user, so the user, usually the bot using an app token for authentication, cannot do anything for some time. Primary limits may reset quickly, but secondary limits need a cool-off period. The file method also fails if the file is so large that after chunking at 65k characters it exceeds the API limit. There is a related note that a file passed with issue-body-file-path must be reachable, so if it lives in your repository you have to check the repository out in the same job as the approval issue.

Behaviour changed at v1.10.0, so read the upgrade note

The readme carries a version specific warning at the top, headed v1.9.x to v1.10.0, about the handling of issue content. From v1.10.0 the issue-body and issue-body-file-path inputs are added as comments on the issue instead of being set as the issue's main description or body, and the issue title is now exactly what is provided as the input rather than being appended to or wrapped in a predefined string. Both changes are sensible. Titles that are pure input values are easier to filter and search in an issues list, and bodies as comments keep the description free for context. They are also breaking changes for anyone who automated against the old shape, for example a script that searched issue bodies rather than comments. Since the current release line is 1.13.x, an adopter on a modern version is already past that boundary. The general lesson is that this action has had behavioural changes across minor versions, so a pin to a specific tag and a deliberate upgrade step is the right pattern for a gate that blocks production releases. The readme also documents an output, approval-status, which is a string that is either approved or denied, so a later step in the workflow can branch on the decision.

Runner support is Linux only, and the build ships as a multi-arch container

The readme is direct about what will not run. The action is compatible with Linux on amd64, meaning 64-bit Intel and AMD x86, with Linux on arm64, meaning 64-bit ARM such as an Apple M1, and with Linux on arm/v8. It is not supported on Windows on amd64, and non-Linux runners of any architecture are not supported. That is a hard constraint rather than a caveat, so a workflow that only has Windows runners cannot use this action. The build system matches the constraint. The Makefile targets linux/amd64, linux/arm64 and linux/arm/v8, and the build and push targets use buildx with an explicit platform list, so the published image exists for exactly the three supported architectures. The Dockerfile builds a static binary with CGO disabled in a golang:1.25 builder stage and copies it into an alpine:3.23 runtime with ca-certificates added, and a comment explains that the image digest is not pinned so that multiple architecture builds work without multiple Dockerfiles. The image is published to ghcr.io/trstringer/manual-approval, the lint target runs golangci-lint in a container, and the test target is a plain go test invocation.

Editorial conclusion

manual-approval is worth adopting for a team on GitHub whose releases or deployments need a human checkpoint and whose repositories are private, since it delivers that gate without the environment configuration GitHub's own review feature requires. It is the wrong choice if you already have environments available and configured, since the native path needs no extra action and no polling, and it is the wrong choice for a Windows runner, which it does not support. Verify first that your approvers have permission to be assigned issues, that the GITHUB_TOKEN in scope can both open and comment on issues, and that your polling interval and the 35 day workflow timeout line up with how fast a human actually responds. The last push was on 2026-09-08 and the newest release is v1.13.1 from 2026-06-27.

Frequently asked questions

How do I add a manual approval step to a GitHub Actions workflow?

Add a step that uses trstringer/manual-approval@v1 with the secret, approvers and issue-title inputs. The action creates an issue, assigns it to the approvers, and the workflow continues only after enough approvers reply with an approval keyword.

Which words count as approve or deny in manual-approval?

approve, approved, lgtm and yes mean approval, while deny, denied and no mean denial. Matching is case insensitive and tolerates an optional trailing period or exclamation mark, and you can extend both lists with the additional-approved-words and additional-denied-words inputs.

Can a user approve their own deployment with manual-approval?

By default yes, because exclude-workflow-initiator-as-approver defaults to false. Set it to true and the workflow initiator, determined by the GITHUB_ACTOR environment variable, is filtered out of the approver list.

What happens if manual-approval cannot assign an approver?

Required approvers must have the ability to be set as approvers in the repository. If you add one without that permission, the action fails with an HTTP/402 Validation Failed error when it runs.

Does manual-approval work on private repositories and Windows runners?

It works on private repositories without needing environments, which is the point of the action, but it runs on Linux runners only, and Windows on amd64 and non-Linux runners of any architecture are not supported.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. trstringer/manual-approval on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/trstringer-manual-approval.svg)](https://hysenlabs.com/projects/trstringer-manual-approval)

Was this useful?