Continuous deployment pipelines demand precision, speed, and strict control over what reaches production environments. A critical bottleneck in this process is the need for human intervention—a manual approval step—before sensitive operations, such as production deployments, proceed. While GitHub provides native environment protection rules that mandate manual approvals, these features are often gated behind GitHub Enterprise plans for private repositories. For teams operating on free or standard tiers, this limitation creates a significant gap in their ability to enforce secure, cost-effective deployment protocols.
The solution lies in community-driven automation. By leveraging specific GitHub Actions, such as pavlospt/manual-approval, developers can pause workflows, solicit approval from designated users or teams via GitHub Issues, and resume execution only after explicit consent is granted. This approach not only democratizes access to approval workflows but also introduces a layer of financial efficiency. Unlike native environment waits, which consume runner minutes and incur costs while idle, custom approval workflows can be architected to minimize resource consumption, ensuring that teams do not pay for waiting time.
The Mechanics of Manual Approval via Issues
The core mechanism of the pavlospt/manual-approval action relies on transforming the approval gate into a GitHub Issue. When a workflow reaches the manual-approval step, the action pauses execution and automatically creates a new issue within the containing repository. This issue is assigned to the specified approvers, signaling that human attention is required. The workflow remains suspended until the issue is resolved through specific keyword comments, effectively turning a standard bug tracker or task list into a robust access control checkpoint.
The action supports a flexible configuration of approvers, allowing administrators to designate individual users or entire organization teams. This is particularly useful for large teams where approval authority might shift based on shifts or project phases. The action requires the GITHUB_TOKEN or a custom GitHub App token to create the issue and interact with the repository. Crucially, the workflow checks for specific keywords in the comments to determine the next course of action.
The approved keywords include "approve", "approved", "lgtm", and "yes". Conversely, denied keywords include "deny", "denied", and "no". These keywords are case-insensitive and tolerate optional punctuation, such as periods or exclamation marks. If all designated approvers comment with an approved keyword, the workflow resumes. If any approver comments with a denied keyword, the workflow immediately exits with a failed status. In all cases, once the decision is made, the action automatically closes the initial GitHub issue, keeping the repository clutter-free.
A critical constraint to consider is the broader workflow timeout. GitHub Actions workflows have a maximum duration of 72 hours. While the approval step is paused, it consumes this time. Therefore, teams must ensure that approvers are responsive enough to meet this deadline, or they risk the workflow terminating due to timeout before a decision is rendered.
Cost Implications and Resource Management
One of the most compelling reasons to adopt a custom approval workflow is the financial impact of traditional methods. When using native GitHub Environments, the workflow runner remains active and allocated while waiting for approval. This means the team is burning through their monthly Actions minutes and incurring compute costs for a job that is essentially doing nothing. For high-frequency deployment pipelines, these idle minutes can accumulate into a significant expense.
However, implementing a manual approval action does not entirely eliminate compute costs if not configured correctly. When the pavlospt/manual-approval action pauses the job, the runner instance (the virtual machine) may still be running. A paused job continues to consume a concurrent job allocation out of the maximum allowed concurrent jobs for the repository. Furthermore, if the job remains paused for too long, it will eventually fail. Specifically, a job is failed after 6 hours of runtime, regardless of its status. This means that while the workflow is paused, it is still technically "running" from the perspective of the GitHub infrastructure, potentially incurring costs and occupying concurrency slots.
To mitigate this, developers often combine the manual approval action with careful timeout settings. For instance, setting a timeout-minutes parameter of 60 ensures that if no action is taken within an hour, the job fails cleanly rather than lingering. This prevents the accumulation of stale jobs that block other pipeline runs. Additionally, using GitHub App tokens instead of the default GITHUB_TOKEN can provide finer-grained control over permissions and expiry, although it introduces its own constraints. GitHub App tokens expire after one hour, meaning that if the approval process takes longer than 60 minutes, the token will expire, and the job will fail due to bad credentials. This necessitates a careful balance between security, token validity, and approval speed.
Configuration and Permissions
Implementing the manual approval action requires precise configuration in the workflow file. The action needs write permissions to create and update issues in the repository. This is typically achieved by adding a permissions block to the workflow YAML file, specifying issues: write. Without this permission, the action will fail to create the issue, and the workflow will not pause as intended.
The action accepts several inputs to customize the approval process. The secret parameter is used to pass the authentication token, which can be the standard GITHUB_TOKEN or a custom token generated via a GitHub App. The approvers parameter is a comma-delimited list of users or organization teams who must approve the workflow. The minimum-approvals parameter specifies how many of the listed approvers must comment to satisfy the condition. For example, if three approvers are listed but minimum-approvals is set to 1, the workflow will proceed once any single approver comments with an approved keyword.
Additional customization is available through issue-title and issue-body parameters, allowing teams to provide context to the approvers. For instance, a title like "Deploying v1.3.5 to prod from staging" clearly communicates the intent. The exclude-workflow-initiator-as-approver flag can be set to true to prevent the person who triggered the workflow from approving it themselves, enforcing separation of duties. Teams can also define additional-approved-words and additional-denied-words to expand the set of recognized commands.
yaml
steps:
- uses: pavlospt/manual-approval@v2
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."
exclude-workflow-initiator-as-approver: false
additional-approved-words: ''
additional-denied-words: ''
For organizations using GitHub Apps, the token generation step must be included before the approval step. The tibdex/github-app-token action can generate a short-lived token using the app's ID and private key stored in secrets.
yaml
jobs:
myjob:
runs-on: ubuntu-latest
steps:
- name: Generate token
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.APP_ID }}
private_key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Wait for approval
uses: pavlospt/manual-approval@v2
with:
secret: ${{ steps.generate_token.outputs.token }}
approvers: myteam
minimum-approvals: 1
Advanced Approval Workflows and Custom Logic
While pavlospt/manual-approval provides a straightforward pause-and-resume mechanism, more complex workflows may require custom logic to verify approvals and extract data from issues. This approach involves using the issue_comment event to trigger a workflow that checks for approval keywords and authorizes the deployer.
In such a setup, the workflow is triggered when a comment is made on an issue. The first step is to verify that the comment contains an approved keyword and that the author is an authorized user or team member. This can be achieved using the actions/github-script action, which allows for JavaScript-based logic within the workflow. The script checks the payload of the event, validates the user, and sets an output variable indicating whether the user is authorized.
Once authorization is confirmed, the workflow can proceed to extract relevant data from the issue body, such as a commit SHA, and use it to checkout the specific code version to be deployed. This ensures that the deployment is tied to a specific commit, providing traceability and reproducibility. The workflow then verifies that the checked-out commit matches the expected SHA, adding an extra layer of integrity checking. Finally, the deployment steps are executed, and the approval issue is closed.
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Check approver
id: check-approver
uses: actions/github-script@v7
with:
script: |
const author = context.payload.comment.user.login;
const authorizedUsers = ['user1', 'user2'];
const approvedKeywords = ['approve', 'approved', 'lgtm', 'yes'];
const commentBody = context.payload.comment.body.toLowerCase();
let isAuthorized = authorizedUsers.includes(author);
let isApproved = approvedKeywords.some(keyword => commentBody.includes(keyword));
if (isAuthorized && isApproved) {
core.setOutput('status', 'authorized');
console.log(`Approved by: @${author}`);
} else {
core.setOutput('status', 'unauthorized');
core.setFailed('Approval must come from an authorized user or team member');
}
- name: Extract commit SHA
id: extract-sha
if: steps.check-approver.outputs.status == 'authorized'
uses: actions/github-script@v7
with:
script: |
const issueBody = context.payload.issue.body;
const match = issueBody.match(/"commit_sha":\s*"([a-f0-9]+)"/);
if (!match) {
core.setFailed('Could not find commit SHA in issue body');
return;
}
const commitSha = match[1];
core.setOutput('commit_sha', commitSha);
console.log(`Extracted commit SHA: ${commitSha}`);
- uses: actions/checkout@v2
if: steps.check-approver.outputs.status == 'authorized'
with:
ref: ${{ steps.extract-sha.outputs.commit_sha }}
- name: Verify correct commit
if: steps.check-approver.outputs.status == 'authorized'
run: |
current_sha=$(git rev-parse HEAD)
expected_sha=${{ steps.extract-sha.outputs.commit_sha }}
if [ "$current_sha" != "$expected_sha" ]; then
echo "Error: Checked out SHA ($current_sha) does not match expected SHA ($expected_sha)"
exit 1
fi
echo "Verified correct commit SHA: $current_sha"
- name: Deploy to Staging
if: steps.check-approver.outputs.status == 'authorized'
run: |
echo "Deploying commit ${{ steps.extract-sha.outputs.commit_sha }} to staging..."
- name: Close Approval Issue
if: steps.check-approver.outputs.status == 'authorized'
uses: actions/github-script@v7
with:
script: |
const issue_number = context.payload.issue.number;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue_number,
body: 'Deployment completed successfully. Issue closed.'
});
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue_number,
state: 'closed'
});
```
This custom approach offers greater flexibility, allowing teams to integrate approval logic directly into their deployment scripts. It also avoids the potential pitfalls of the pavlospt/manual-approval action, such as the consumption of runner minutes while waiting. By triggering the workflow only when an approval comment is received, the compute resources are only used when necessary, further optimizing costs.
Testing and Development of Custom Actions
For organizations that wish to develop their own approval actions or modify existing ones, GitHub provides tools for testing and deployment. To test a custom action, developers can build the action's Docker image and push it to a container registry. This allows for iterative testing without affecting production workflows.
The process involves setting a version number, building the image, and pushing it to a registry such as GitHub Container Registry (ghcr.io). The action.yaml file is then modified to point to the new image. A workflow can be configured to use the development branch of the custom action, allowing developers to test the approval process in a controlled environment.
bash
VERSION=1.7.1-rc.1 make IMAGE_REPO=ghcr.io/pavlospt/manual-approval-test push
The action.yaml file should be updated to reference the test image:
yaml
image: docker://ghcr.io/pavlospt/manual-approval-test:1.7.0-rc.1
Then, the workflow can be updated to use the development branch:
yaml
- name: Wait for approval
uses: your-github-user/manual-approval@your-dev-branch
with:
secret: ${{ secrets.GITHUB_TOKEN }}
approvers: pavlospt
Once testing is complete, the new version can be built, pushed, and tagged. A release branch is created, and the action.yaml is updated to point to the new stable image. The changes are merged into the main branch, and new tags are created and pushed to the repository. Finally, a GitHub release is created to make the new version available to users.
bash
VERSION=1.7.0 make build
VERSION=1.7.0 make push
git tag -d v1 && git push --delete origin v1
git tag v1.7.0 && git tag v1 && git push origin --tags
This rigorous testing and release process ensures that custom approval actions are reliable and secure, providing teams with confidence in their deployment pipelines.
Conclusion
Manual approval workflows are a critical component of modern DevOps practices, ensuring that changes are reviewed and authorized before reaching production environments. While GitHub Enterprise offers native support for this functionality, teams on lower-tier plans can leverage community-driven actions like pavlospt/manual-approval to achieve similar results. By understanding the mechanics, cost implications, and configuration requirements of these actions, organizations can implement robust, cost-effective approval workflows that maintain security without breaking the bank. Whether using simple issue-based approvals or custom logic-driven workflows, the key is to balance control, efficiency, and cost, ensuring that the deployment pipeline remains both secure and agile.