Manual approval steps serve as critical checkpoints in a Continuous Integration and Continuous Deployment (CI/CD) pipeline, pausing execution until a human reviewer explicitly approves or rejects the subsequent action. In production environments, these gates are not merely optional conveniences but essential mechanisms for risk mitigation, cost control, and regulatory compliance. They prevent the accidental or premature deployment of costly resources, ensure that user-facing features or critical bug fixes have undergone human review, and satisfy internal or external compliance mandates that require explicit sign-off before specific actions occur. While early DevOps practices often relied on fragile workarounds like Slack messages or custom scripts, modern CI/CD platforms have moved toward first-class, native approval mechanisms that are secure, auditable, and integrated directly into the deployment workflow.
GitHub Actions offers two distinct architectural approaches to implementing manual approvals. The primary, native method utilizes GitHub Environments and deployment protection rules, providing enterprise-grade controls without consuming runner resources during the wait period. Alternatively, for users in public repositories or those wishing to avoid the overhead of environment configuration, third-party actions provide a flexible, issue-based approval mechanism that works across repository visibility tiers. Understanding the nuances, configurations, and trade-offs of both approaches allows engineering teams to select the most appropriate strategy for their specific deployment topology and security requirements.
The Native Approach: GitHub Environments and Protection Rules
The standard and most robust method for implementing manual approvals in GitHub Actions relies on the concept of Environments. An Environment represents a logical deployment target, such as dev, staging, or prod. By associating specific jobs within a workflow with an environment and configuring deployment protection rules, GitHub Actions can natively enforce a pause-and-wait state that requires explicit human intervention.
This approach is native to the platform, secure, and fully auditable. Unlike custom scripts that might bypass security checks, the native environment mechanism integrates directly with GitHub’s permission model. Crucially, this method does not consume Runner resources while waiting for approval. When a job is assigned to an environment with a required reviewer rule, the job enters a "Pending" state. The runner is released, saving computational costs and allowing other workflows to proceed, while the job simply waits in the GitHub UI for the designated reviewers to act.
Configuring Environment Protection Rules
To implement this, administrators must first define the environment and its protection rules within the repository settings. This involves navigating to Settings > Environments, creating or selecting an existing environment (e.g., production), and configuring "Deployment protection rules." Within these rules, users can specify "Required reviewers," which can be individual users or entire GitHub Teams. For example, a team might create a @release-approvers team containing senior developers and security leads, then assign this team as the required reviewer for the production environment. This ensures that both a development lead and a security lead must approve before the job proceeds, or that any member of the designated team can provide the necessary sign-off.
Implementing the Workflow Job
Once the environment and reviewers are configured, the workflow file must reference this environment. A typical workflow involves a build-and-test job followed by a deployment job. The deployment job must explicitly declare the target environment using the environment key.
Consider a workflow file named .github/workflows/manual-approval-demo.yml. The structure ensures that the deployment job only runs after the build and test jobs have succeeded, and it pauses when it reaches the production environment step.
```yaml
name: Manual Approval Demo
on:
push:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Build and Test
run: echo "Running build and test suite..."
deploy-to-production:
needs: build-and-test
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to Production
run: echo "Deploying to production..."
```
In this configuration, the needs: build-and-test directive ensures sequential execution. When the deploy-to-production job starts, GitHub Actions recognizes the environment: production declaration. Because the production environment has a required reviewer rule, the job status changes to "Waiting" with a prompt stating "Waiting for environment approval." The job does not proceed to the run step until an authorized user intervenes.
Reviewing and Approving Deployments
When a workflow reaches the pending state, the designated reviewers receive notifications via email, the GitHub notification center, and mobile push notifications if enabled. To approve the deployment, the reviewer navigates to the Actions tab of the repository, selects the specific workflow run, and clicks on the pending job (e.g., deploy-to-production). They then click "Review deployments," which opens a modal with Approve and Reject buttons. Clicking Approve releases the job, allowing it to resume execution on the runner. Clicking Reject causes the job to fail and terminates the workflow. This process provides a clear audit trail, logging who approved the deployment and when.
Customization: Timeouts and Notifications
By default, jobs waiting for approval will pause indefinitely. However, leaving a pipeline in a pending state for too long can clutter the workflow history and delay feedback loops. To mitigate this, administrators can configure a timeout. This is achieved by adding the timeout-minutes parameter to the job definition in the workflow file. If the approval is not received within the specified window, the job fails automatically.
yaml
deploy-to-production:
needs: build-and-test
runs-on: ubuntu-latest
environment: production
timeout-minutes: 30
steps:
- name: Deploy to Production
run: echo "Deploying to production..."
Beyond native notifications, teams can enhance visibility by integrating external communication channels. Workflow steps can be added to send custom notifications via Slack or Microsoft Teams when a job enters the pending state. For instance, using the actions-ecosystem/action-slack-notifier action allows teams to broadcast pending approval requests directly to their preferred collaboration tools, ensuring that the right people are alerted immediately without needing to check the GitHub UI constantly.
The Alternative Approach: Third-Party Manual Approval Actions
While the native environment approach is powerful, it has certain constraints. It requires the use of Environments, a feature that may not align with every team’s workflow architecture. More significantly, in public repositories, the "Required reviewers" feature for environments is only available to users with GitHub Enterprise or specific plan tiers. For open-source projects or teams in private repositories without Enterprise licenses, a native pause-and-approve mechanism might be inaccessible or overly complex to manage.
To address these limitations, third-party actions have emerged that implement manual approvals through a different mechanism: GitHub Issues. These actions allow any repository, regardless of visibility or plan tier, to pause a workflow until a designated set of users comments on a specific issue.
How Issue-Based Approval Works
Actions such as trstringer/manual-approval or akefirad/manual-approval-action work by creating a GitHub Issue in the repository associated with the workflow run. The issue is automatically assigned to the specified approvers. The workflow then enters a polling state, waiting for specific keywords to appear in the comments of that issue.
The workflow continues only when all designated approvers have responded with an approval keyword (e.g., "approved", "LGTM!"). Conversely, if any approver responds with a denial keyword (e.g., "deny", "Denied!"), the workflow fails immediately. This approach decouples the approval logic from the runner, as the waiting state is managed by the action’s polling mechanism rather than the runner’s idle time.
Implementing the Manual Approval Action
To use this approach, a team adds a specific step in their workflow that utilizes the third-party action. The action requires a GitHub token to create issues and poll comments, as well as a list of approvers.
yaml
steps:
- name: Manual Approval
uses: trstringer/manual-approval@v1
with:
secret: ${{ github.TOKEN }}
approvers: user1,user2
In this example, the action creates an issue assigned to user1 and user2. The workflow pauses at this step. Both users must comment with an approval keyword for the next steps to execute. If either user comments with a denial keyword, the job fails. This method is particularly useful for complex approvals that require detailed context, as the issue body can be customized to include deployment details, allowing reviewers to discuss the changes before approving.
Advanced Configuration and Context Variables
More advanced actions, such as akefirad/manual-approval-action, offer greater flexibility in configuring approval and rejection keywords, timeouts, and issue content. These actions allow teams to define specific keywords for approval and rejection, ensuring that only explicit, unambiguous comments trigger the next stage.
yaml
- name: Database Migration Approval
uses: akefirad/manual-approval-action@main
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
approval-keywords: "deploy,approved!"
rejections-keywords: "cancel,reject!"
timeout-seconds: 900
issue-title: "⚠️ Database Migration Approval Required"
issue-body: |
## Database Migration Details
**Environment**: Production
**Workflow**: {{ workflow-name }}
**Migration**: Add user_preferences table
**Estimated downtime**: 5 minutes
Please review the migration script and respond with:
- `approved!` to approve the migration
- `reject!` to cancel
⏰ This request will timeout in {{ timeout-seconds }} seconds.
This configuration creates a highly informative issue, providing reviewers with critical context such as the environment, workflow name, specific migration details, and estimated downtime. It also sets a strict timeout of 900 seconds (15 minutes), after which the approval request expires. The action supports various GitHub context variables, such as ${{ github.repository }}, ${{ github.run_id }}, and ${{ github.actor }}, allowing for dynamic and precise issue generation. This level of detail is invaluable for high-stakes operations, such as database migrations, where reviewers need to understand the impact before granting approval.
Comparing GitHub Actions to Other CI/CD Tools
Understanding how GitHub Actions handles manual approvals in the context of other popular CI/CD platforms highlights the strengths and trade-offs of each approach. Jenkins, CircleCI, and GitLab CI/CD each have their own mechanisms for implementing human-in-the-loop steps.
Jenkins: Input Steps
Jenkins Pipelines use the input step to explicitly define manual approval checkpoints within the pipeline script. This step pauses the execution of the pipeline and waits for a user to interact with the Jenkins UI to approve the step.
The key difference lies in resource management. Jenkins’s input step requires a running Agent to hold the pipeline state, meaning that runner resources are consumed while waiting for approval. In contrast, GitHub Actions’s environment-based approval releases the runner immediately upon entering the pending state, making it more efficient in terms of resource utilization. Jenkins’s approach is more embedded in the script logic, whereas GitHub Actions abstracts the approval into the environment configuration, providing a cleaner separation between workflow logic and deployment security policies.
CircleCI: Approval Jobs
CircleCI implements manual approvals using a specific job type: type: approval. This job acts as a gate in the workflow, requiring manual intervention to proceed.
While similar in outcome to GitHub Actions, CircleCI’s approval jobs are defined directly within the workflow configuration. GitHub Actions, through its environment model, offers a richer context for approvals. Environments not only enforce approvals but also scope secrets and variables to specific deployment targets. This means that the production environment can have unique secrets that are not accessible in staging, and the approval rule is tied to the concept of the deployment target itself, rather than just a step in the workflow. This provides additional layers of security and organizational clarity.
GitLab CI/CD: Manual Actions
GitLab CI/CD uses the when: manual keyword to pause jobs. This is a job-level control that allows users to manually trigger a job from the GitLab UI by clicking a "Play" button.
The distinction here is in the mechanism of approval. GitLab’s manual trigger is a direct user action to start a job. GitHub Actions’s environment approval is a protective rule that blocks a job until a specific set of authorized users explicitly approve it. GitHub Actions also supports more granular protection rules, such as waiting for a specific number of approvals, enforcing branch restrictions, and setting wait times, offering a more robust security model for enterprise deployments. GitLab’s approach is simpler but may lack the fine-grained access control and auditability provided by GitHub’s environment protection rules.
Troubleshooting and Best Practices
Implementing manual approvals is straightforward, but several common issues can arise if configurations are not meticulously managed. Understanding these pitfalls ensures that the approval process remains reliable and secure.
Job Isn’t Pausing for Approval
If a job assigned to an environment does not pause for approval, the issue is typically configuration-related. The most common cause is that the environment has not been properly configured with "Required reviewers." Administrators must verify that the environment settings in Settings > Environments > [Environment Name] include the necessary reviewer rules. Another potential cause is that the workflow file does not correctly reference the environment. Every job that requires approval must explicitly include the environment: <environment_name> key. Without this declaration, the job will run normally, bypassing the protection rules entirely.
Approval Not Received by Reviewers
If designated reviewers do not receive approval requests, the issue often stems from permissions. Reviewers must have at least "Collaborator" status in the repository to be assigned to an environment’s protection rules. If a user is an outside collaborator without sufficient permissions, they cannot be added as a required reviewer. Additionally, reviewers should ensure that their notification settings are configured correctly to receive emails and GitHub notifications. Integrating external notification channels, such as Slack, can help ensure that reviewers are promptly alerted to pending approvals.
Best Practices for Implementation
To maximize the effectiveness of manual approvals, teams should adopt several best practices. First, use GitHub Teams to manage reviewers rather than assigning individual users. This simplifies administration, as team membership changes are handled centrally, and the workflow configuration remains stable. Second, implement timeouts to prevent workflows from hanging indefinitely. Even in critical production environments, a timeout ensures that stale approval requests do not clutter the system. Third, provide clear context in the approval request. Whether using native environments or third-party actions, include relevant details such as the commit SHA, workflow name, and specific changes being deployed. This enables reviewers to make informed decisions quickly. Finally, ensure that approval rules are applied consistently across all critical environments, including staging and production, to maintain a uniform security posture.
Conclusion
Manual approval steps are a cornerstone of secure and reliable CI/CD pipelines, providing a critical check against accidental deployments, unauthorized changes, and costly errors. GitHub Actions offers two robust mechanisms for implementing these approvals: the native Environment protection rules and third-party issue-based actions. The native approach is ideal for teams with GitHub Enterprise or those seeking deep integration with GitHub’s security model, offering resource-efficient pausing and comprehensive audit trails. The third-party approach provides flexibility for public repositories or teams who prefer to avoid environment complexity, leveraging GitHub Issues to facilitate rich, contextual approval workflows.
By understanding the technical details of both methods—including configuration syntax, timeout handling, notification integration, and troubleshooting common pitfalls—engineering teams can tailor their approval processes to their specific needs. Whether mitigating risk in a high-stakes production deployment or streamlining approvals for internal tools, GitHub Actions provides the tools necessary to enforce human oversight without sacrificing the speed and agility of modern DevOps practices. The choice between native environments and third-party actions ultimately depends on the repository’s visibility, the team’s existing workflow architecture, and the level of granularity required in the approval process.