Complete, copy-paste-ready workflow examples.
- Minimal Example
- Event-Driven Pattern
- Blocking Pattern
- Multi-Environment Promotion
- Full-Featured Request
- Team Support with GitHub App
- Using Outputs in Subsequent Jobs
- Handle Issue Close Events
The quickest path to a working approval workflow:
1. Create .github/approvals.yml:
version: 1
policies:
approvers:
approvers: [alice, bob]
min_approvals: 1
workflows:
deploy:
require:
- policy: approvers
on_approved:
create_tag: true
close_issue: true2. Create .github/workflows/request-approval.yml:
name: Request Approval
on:
workflow_dispatch:
inputs:
version:
required: true
type: string
permissions:
contents: write
issues: write
jobs:
request:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jamengual/enterprise-approval-engine@v1
with:
action: request
workflow: deploy
version: ${{ inputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}3. Create .github/workflows/handle-approval.yml:
name: Handle Approval
on:
issue_comment:
types: [created]
permissions:
contents: write
issues: write
jobs:
process:
if: |
github.event.issue.pull_request == null &&
contains(github.event.issue.labels.*.name, 'approval-required')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jamengual/enterprise-approval-engine@v1
with:
action: process-comment
issue_number: ${{ github.event.issue.number }}
token: ${{ secrets.GITHUB_TOKEN }}That's it! Trigger the workflow, then approve by commenting approve on the issue.
The recommended pattern for most use cases. Approval is decoupled from deployment.
request-approval.yml:
name: Request Deployment Approval
on:
workflow_dispatch:
inputs:
environment:
type: choice
options: [staging, production]
version:
required: true
permissions:
contents: write
issues: write
jobs:
request:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jamengual/enterprise-approval-engine@v1
id: approval
with:
action: request
workflow: ${{ inputs.environment }}-deploy
version: ${{ inputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Summary
run: |
echo "## Approval Request Created" >> $GITHUB_STEP_SUMMARY
echo "- **Issue:** ${{ steps.approval.outputs.issue_url }}" >> $GITHUB_STEP_SUMMARYhandle-approval.yml:
name: Handle Approval Comments
on:
issue_comment:
types: [created]
permissions:
contents: write
issues: write
jobs:
process:
if: |
github.event.issue.pull_request == null &&
contains(github.event.issue.labels.*.name, 'approval-required')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jamengual/enterprise-approval-engine@v1
id: process
with:
action: process-comment
issue_number: ${{ github.event.issue.number }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Deployment
if: steps.process.outputs.status == 'approved'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh workflow run deploy.yml \
-f version=${{ steps.process.outputs.tag }}Workflow waits for approval before proceeding. Consumes runner minutes while waiting.
name: Deploy with Blocking Approval
on:
workflow_dispatch:
inputs:
version:
required: true
permissions:
contents: write
issues: write
jobs:
request-approval:
runs-on: ubuntu-latest
outputs:
issue_number: ${{ steps.request.outputs.issue_number }}
steps:
- uses: actions/checkout@v4
- uses: jamengual/enterprise-approval-engine@v1
id: request
with:
action: request
workflow: production-deploy
version: ${{ inputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
wait-for-approval:
needs: request-approval
runs-on: ubuntu-latest
outputs:
status: ${{ steps.check.outputs.status }}
tag: ${{ steps.check.outputs.tag }}
steps:
- uses: actions/checkout@v4
- uses: jamengual/enterprise-approval-engine@v1
id: check
with:
action: check
issue_number: ${{ needs.request-approval.outputs.issue_number }}
wait: 'true' # Poll until approved/denied
timeout: '4h' # Max wait time
token: ${{ secrets.GITHUB_TOKEN }}
deploy:
needs: [request-approval, wait-for-approval]
if: needs.wait-for-approval.outputs.status == 'approved'
runs-on: ubuntu-latest
steps:
- name: Deploy
run: echo "Deploying ${{ needs.wait-for-approval.outputs.tag }}"Different approval requirements per environment:
.github/approvals.yml:
version: 1
policies:
dev-team:
approvers: [dev1, dev2, dev3]
min_approvals: 1
qa-team:
approvers: [qa1, qa2]
min_approvals: 1
prod-team:
approvers: [team:sre, tech-lead]
min_approvals: 2
workflows:
dev-deploy:
require:
- policy: dev-team
on_approved:
tagging:
enabled: true
auto_increment: patch
env_prefix: "dev-"
close_issue: true
staging-deploy:
require:
- policy: qa-team
on_approved:
tagging:
enabled: true
auto_increment: minor
env_prefix: "staging-"
close_issue: true
production-deploy:
require:
- policy: prod-team
on_approved:
create_tag: true
close_issue: true
on_closed:
delete_tag: false # Never delete production tagsRequest workflow with Jira integration and deployment tracking:
name: Request Production Deployment
on:
workflow_dispatch:
inputs:
version:
description: 'Version to deploy'
required: true
environment:
description: 'Target environment'
required: true
type: choice
options: [staging, production]
permissions:
contents: write
issues: write
deployments: write
jobs:
request:
runs-on: ubuntu-latest
outputs:
issue_number: ${{ steps.approval.outputs.issue_number }}
issue_url: ${{ steps.approval.outputs.issue_url }}
deployment_id: ${{ steps.approval.outputs.deployment_id }}
jira_issues: ${{ steps.approval.outputs.jira_issues }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Needed for commit comparison
- uses: jamengual/enterprise-approval-engine@v1
id: approval
with:
action: request
workflow: ${{ inputs.environment }}-deploy
version: ${{ inputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
# Jira integration
jira_base_url: https://mycompany.atlassian.net
jira_user_email: ${{ secrets.JIRA_EMAIL }}
jira_api_token: ${{ secrets.JIRA_API_TOKEN }}
# Deployment tracking
create_deployment: 'true'
deployment_environment: ${{ inputs.environment }}
deployment_environment_url: https://${{ inputs.environment }}.myapp.com
- name: Summary
run: |
echo "## Approval Request Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Issue:** #${{ steps.approval.outputs.issue_number }}" >> $GITHUB_STEP_SUMMARY
echo "- **URL:** ${{ steps.approval.outputs.issue_url }}" >> $GITHUB_STEP_SUMMARY
echo "- **Jira Issues:** ${{ steps.approval.outputs.jira_issues }}" >> $GITHUB_STEP_SUMMARY
echo "- **Commits:** ${{ steps.approval.outputs.commits_count }}" >> $GITHUB_STEP_SUMMARYUse a GitHub App for team membership checks:
name: Handle Approval Comments
on:
issue_comment:
types: [created]
permissions:
contents: write
issues: write
jobs:
process:
if: |
github.event.issue.pull_request == null &&
contains(github.event.issue.labels.*.name, 'approval-required')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Generate GitHub App token for team membership checks
- uses: actions/create-github-app-token@v2
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: jamengual/enterprise-approval-engine@v1
id: process
with:
action: process-comment
issue_number: ${{ github.event.issue.number }}
token: ${{ steps.app-token.outputs.token }}
# Optional: Update Jira Fix Version on approval
jira_base_url: https://mycompany.atlassian.net
jira_user_email: ${{ secrets.JIRA_EMAIL }}
jira_api_token: ${{ secrets.JIRA_API_TOKEN }}
- name: Trigger Deployment
if: steps.process.outputs.status == 'approved'
uses: actions/github-script@v7
with:
script: |
await github.rest.actions.createWorkflowDispatch({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'deploy.yml',
ref: 'main',
inputs: { version: '${{ steps.process.outputs.tag }}' }
});Pass approval outputs to downstream jobs:
name: Deploy with Approval
on:
workflow_dispatch:
inputs:
version:
required: true
jobs:
approval:
runs-on: ubuntu-latest
outputs:
status: ${{ steps.check.outputs.status }}
tag: ${{ steps.check.outputs.tag }}
approvers: ${{ steps.check.outputs.approvers }}
jira_issues: ${{ steps.request.outputs.jira_issues }}
steps:
- uses: actions/checkout@v4
- uses: jamengual/enterprise-approval-engine@v1
id: request
with:
action: request
workflow: production-deploy
version: ${{ inputs.version }}
token: ${{ secrets.GITHUB_TOKEN }}
jira_base_url: https://mycompany.atlassian.net
- uses: jamengual/enterprise-approval-engine@v1
id: check
with:
action: check
issue_number: ${{ steps.request.outputs.issue_number }}
wait: 'true'
timeout: '2h'
token: ${{ secrets.GITHUB_TOKEN }}
deploy:
needs: approval
if: needs.approval.outputs.status == 'approved'
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy
run: |
echo "Deploying ${{ needs.approval.outputs.tag }}"
echo "Approved by: ${{ needs.approval.outputs.approvers }}"
echo "Jira Issues: ${{ needs.approval.outputs.jira_issues }}"
notify:
needs: [approval, deploy]
if: always()
runs-on: ubuntu-latest
steps:
- name: Notify Slack
run: |
if [ "${{ needs.approval.outputs.status }}" == "approved" ]; then
echo "Deployment of ${{ needs.approval.outputs.tag }} completed!"
else
echo "Deployment was ${{ needs.approval.outputs.status }}"
fiDelete tags when approval issues are manually closed:
name: Handle Issue Close
on:
issues:
types: [closed]
jobs:
handle:
if: contains(github.event.issue.labels.*.name, 'approval-required')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jamengual/enterprise-approval-engine@v1
id: close
with:
action: close-issue
issue_number: ${{ github.event.issue.number }}
issue_action: ${{ github.event.action }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Report
run: |
echo "Status: ${{ steps.close.outputs.status }}"
echo "Deleted tag: ${{ steps.close.outputs.tag_deleted }}"