How to Implement Manual Approval Gates for Critical Workflow Operations with gh-aw
Add manual-approval: <environment-name> to your workflow's front-matter on: section, and the gh-aw compiler automatically configures GitHub environment protection rules to pause execution until a human approves the deployment.
The github/gh-aw compiler transforms markdown-based workflow definitions into executable GitHub Actions by injecting activation jobs that respect environment protection rules. Implementing manual approval gates for critical workflow operations ensures that sensitive deployments require explicit human authorization before proceeding, leveraging GitHub's native environment protection capabilities.
Declaring Manual Approval in Workflow Front-matter
To enable a manual approval gate, declare the manual-approval key inside the on: section of your workflow's YAML front-matter. The value must be a string matching a protected environment name configured in your repository settings.
---
description: Deploy to production only after human approval
on:
workflow_dispatch:
manual-approval: production # <-- protected environment name
permissions:
contents: read
engine: copilot
---
# Your workflow steps follow here
The manual-approval key can coexist with any trigger type, such as workflow_dispatch, issues, or schedule. When the compiler processes this definition, it extracts the environment name and propagates it through the compilation pipeline.
How the Compiler Processes Approval Gates
The implementation spans four core components that extract, store, annotate, and enforce the manual approval configuration.
Extracting the Approval Configuration
File: pkg/workflow/manual_approval.go
The extractManualApprovalFromOn function parses the front-matter map and retrieves the manual-approval value when the on section is structured as a map.
func (c *Compiler) extractManualApprovalFromOn(frontmatter map[string]any) (string, error) {
onSection, exists := frontmatter["on"]
if !exists {
return "", nil
}
switch on := onSection.(type) {
case map[string]any:
if manualApproval, exists := on["manual-approval"]; exists {
if str, ok := manualApproval.(string); ok {
return str, nil
}
return "", fmt.Errorf("manual-approval value must be a string, got %T", manualApproval)
}
}
return "", nil
}
This function returns the environment name as a string or an empty string if the configuration is absent. It returns an error only when the value is present but not a string, ensuring type safety.
Storing Approval Data in Workflow Structures
File: pkg/workflow/manual_approval.go and pkg/workflow/compiler_types.go
The processManualApprovalConfiguration method invokes the extractor and assigns the result to the WorkflowData struct:
manualApproval, err := c.extractManualApprovalFromOn(frontmatter)
if err != nil {
return err
}
workflowData.ManualApproval = manualApproval
The WorkflowData struct definition in compiler_types.go includes:
type WorkflowData struct {
// … other fields …
ManualApproval string // environment name for manual‑approval from on: section
}
Generating Lock File Annotations
File: pkg/workflow/compiler_yaml.go
When generating the compiled lock file, the compiler injects a comment to document the approval requirement:
if data.ManualApproval != "" {
cleanManualApproval := stringutil.StripANSIEscapeCodes(data.ManualApproval)
fmt.Fprintf(yaml, "# Manual approval required: environment '%s'\n", cleanManualApproval)
}
This annotation ensures that developers reviewing the compiled workflow understand that an approval gate is active.
Configuring the Activation Job Environment
File: pkg/workflow/compiler_activation_jobs.go
The compiler constructs the activation job that triggers the main workflow. If ManualApproval is set, it adds the environment key:
var environment string
if data.ManualApproval != "" {
// Strip ANSI escape codes from manual‑approval environment name
cleanManualApproval := stringutil.StripANSIEscapeCodes(data.ManualApproval)
environment = fmt.Sprintf("environment: %s", cleanManualApproval)
}
// …
job := &Job{
Name: string(constants.ActivationJobName),
Environment: environment,
// …
}
When GitHub Actions executes this job, it evaluates the environment: field. If the environment is protected with a "Required reviewers" or "Require approval before job can proceed" rule, the workflow pauses until an authorized user approves it via the GitHub UI or API.
End-to-End Workflow Execution Flow
The complete lifecycle of a manual approval gate involves five distinct stages:
- Declaration: The workflow author adds
manual-approval: <environment>to the front-matteron:section. - Compilation:
gh aw compileinvokesextractManualApprovalFromOnto parse the value, stores it inWorkflowData.ManualApproval, and generates the lock file with an annotation comment. - Job Generation: The compiler creates an activation job in
compiler_activation_jobs.gothat includesenvironment: <value>. - Execution Pause: When the workflow triggers, GitHub detects the environment protection rules and halts the activation job, displaying a "Review pending" button in the Actions UI.
- Continuation: After a user with appropriate permissions approves the deployment, the activation job completes and triggers the main agent job to proceed with the critical operation.
Practical Implementation Examples
Basic Workflow with Production Approval
Create a file at .github/workflows/deploy-prod.md with the following content:
---
description: Production deployment with mandatory human approval
on:
workflow_dispatch:
manual-approval: production
permissions:
contents: read
deployments: write
engine: copilot
---
## Deployment Steps
1. Build container image
2. Run security scans
3. Deploy to production cluster
After running gh aw compile, the generated lock file will contain:
# Manual approval required: environment 'production'
jobs:
activation:
environment: production
# ... additional auto-generated configuration ...
Programmatic Configuration Access
For custom tooling that needs to validate or extract manual approval settings:
package main
import (
"log"
"github.com/github/gh-aw/pkg/workflow"
)
func main() {
compiler := workflow.NewCompiler()
// Parse frontmatter from markdown content
frontmatter, err := compiler.ParseFrontmatter(mdContent)
if err != nil {
log.Fatalf("Failed to parse frontmatter: %v", err)
}
// Extract manual approval configuration
approvalEnv, err := compiler.ExtractManualApprovalFromOn(frontmatter)
if err != nil {
log.Fatalf("Invalid manual-approval configuration: %v", err)
}
if approvalEnv != "" {
log.Printf("Workflow requires manual approval for environment: %s", approvalEnv)
} else {
log.Println("No manual approval gate configured")
}
}
Validating with Unit Tests
The repository includes comprehensive tests in pkg/workflow/manual_approval_test.go that verify extraction logic:
func TestExtractManualApproval(t *testing.T) {
tests := []struct {
name string
frontmatter map[string]any
want string
wantErr bool
}{
{
name: "valid manual approval in on section",
frontmatter: map[string]any{
"on": map[string]any{
"workflow_dispatch": nil,
"manual-approval": "production",
},
},
want: "production",
wantErr: false,
},
{
name: "missing manual approval",
frontmatter: map[string]any{
"on": map[string]any{
"workflow_dispatch": nil,
},
},
want: "",
wantErr: false,
},
}
// Test execution logic here...
}
Run the test suite with:
make test-unit
# or specifically:
go test ./pkg/workflow/... -run TestManualApproval
Key Source Files Reference
Understanding the manual approval implementation requires familiarity with these specific files in the github/gh-aw repository:
-
pkg/workflow/manual_approval.go– ContainsextractManualApprovalFromOnandprocessManualApprovalConfigurationfunctions that parse and validate the front-matter configuration. -
pkg/workflow/manual_approval_test.go– Unit tests covering valid configurations, missing values, and type validation errors. -
pkg/workflow/manual_approval_integration_test.go– Integration tests verifying that the activation job correctly receives the environment configuration. -
pkg/workflow/compiler_types.go– Defines theWorkflowDatastruct with theManualApprovalstring field. -
pkg/workflow/compiler_yaml.go– Implements the lock file comment generation that documents the approval requirement. -
pkg/workflow/compiler_activation_jobs.go– Constructs the activation job with theenvironment:field that triggers GitHub's protection rules. -
pkg/cli/workflows/test-manual-approval.md– Example workflow file demonstrating the front-matter syntax used by the test suite.
Summary
- Declare manual approval gates by adding
manual-approval: <environment-name>to theon:section of your workflow front-matter. - Compile workflows using
gh aw compileto generate activation jobs that include theenvironment:field required by GitHub's protection rules. - Extract configuration programmatically via
extractManualApprovalFromOninpkg/workflow/manual_approval.gofor custom validation tooling. - Validate implementations using the comprehensive test suite in
pkg/workflow/manual_approval_test.goand integration tests. - Protect critical operations by configuring the target environment in GitHub repository settings with required reviewers or deployment protection rules.
Frequently Asked Questions
How do I configure the required reviewers for a manual approval gate?
Configure the protection rules directly in your GitHub repository settings under Settings > Environments. Select the environment name specified in your manual-approval value (e.g., production), then enable "Required reviewers" and specify which users or teams must approve the deployment. The gh-aw compiler only injects the environment: field into the activation job; GitHub's platform handles the actual approval UI and notifications.
What happens if I specify a manual-approval environment that does not exist?
If the environment name specified in manual-approval does not exist in the repository settings, GitHub Actions will attempt to create it dynamically when the workflow runs. However, dynamically created environments do not inherit protection rules, so the workflow will execute without pausing for approval. Always pre-configure the environment in repository settings with protection rules to ensure the gate functions correctly.
Can I use manual approval gates with triggers other than workflow_dispatch?
Yes, the manual-approval key functions alongside any valid GitHub Actions trigger. You can combine it with issues, pull_request, schedule, or custom repository events. The compiler extracts the value from the on: section regardless of the trigger type, and the activation job will always include the environment: field, causing GitHub to evaluate protection rules before proceeding.
How do I troubleshoot when the manual approval gate is not triggering?
First, verify that the compiled lock file contains the environment declaration by checking for the comment # Manual approval required: environment '...' in the generated YAML. If absent, ensure your front-matter syntax is correct and that gh aw compile completed without errors. If the comment exists but the gate still doesn't trigger, confirm that the environment name in your repository settings exactly matches the value in manual-approval (case-sensitive) and that protection rules are enabled for that environment. Check the workflow run logs to see if the activation job is being created with the correct environment: field.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →