Skip to content

Commit 7b8c792

Browse files
CopilotMossaka
andcommitted
Add support for custom AWF installation path in firewall configuration
Co-authored-by: Mossaka <5447827+Mossaka@users.noreply.github.com>
1 parent d2c70d0 commit 7b8c792

File tree

8 files changed

+569
-11
lines changed

8 files changed

+569
-11
lines changed

docs/src/content/docs/reference/frontmatter-full.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,11 @@ network:
878878
# (optional)
879879
log-level: "debug"
880880

881+
# Custom path to AWF binary. When specified, skips downloading AWF from GitHub
882+
# releases. Supports absolute paths or paths relative to GITHUB_WORKSPACE.
883+
# (optional)
884+
path: "example-value"
885+
881886
# Conditional execution expression
882887
# (optional)
883888
if: "example-value"

docs/src/content/docs/reference/network.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,39 @@ The default log level is `info`, which provides a balance between visibility and
112112

113113
See the [Copilot Engine - Network Permissions](/gh-aw/reference/engines/#network-permissions) documentation for detailed AWF configuration options.
114114

115+
### Custom AWF Binary Path
116+
117+
Specify a custom AWF binary instead of downloading from GitHub releases:
118+
119+
```yaml wrap
120+
network:
121+
firewall:
122+
path: /usr/local/bin/awf-custom # Absolute path
123+
allowed:
124+
- defaults
125+
```
126+
127+
Relative paths are resolved relative to the repository root:
128+
129+
```yaml wrap
130+
network:
131+
firewall:
132+
path: bin/awf # Resolves to ${GITHUB_WORKSPACE}/bin/awf
133+
allowed:
134+
- defaults
135+
```
136+
137+
When `path` is specified:
138+
- AWF is not downloaded from GitHub releases
139+
- The specified binary must exist and be executable
140+
- Path is validated before workflow execution
141+
- The `version` field is ignored (if also specified)
142+
143+
**Use cases:**
144+
- Pre-installed AWF on self-hosted runners
145+
- Custom AWF builds with patches or modifications
146+
- Repository-specific AWF versions
147+
115148
## Best Practices
116149

117150
Follow the principle of least privilege by only allowing access to domains and ecosystems actually needed. Prefer ecosystem identifiers over broad wildcard patterns. Avoid overly permissive patterns like `"*"` or `"*.com"`.

pkg/parser/schemas/main_workflow_schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1675,6 +1675,10 @@
16751675
"type": "string",
16761676
"description": "AWF log level (default: info). Valid values: debug, info, warn, error",
16771677
"enum": ["debug", "info", "warn", "error"]
1678+
},
1679+
"path": {
1680+
"type": "string",
1681+
"description": "Custom path to AWF binary. When specified, skips downloading AWF from GitHub releases. Supports absolute paths or paths relative to GITHUB_WORKSPACE."
16781682
}
16791683
},
16801684
"additionalProperties": false

pkg/workflow/copilot_engine.go

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,24 @@ func (e *CopilotEngine) GetInstallationSteps(workflowData *WorkflowData) []GitHu
6262
steps = append(steps, npmSteps[0]) // Setup Node.js step
6363
}
6464

65-
// Add AWF installation steps only if firewall is enabled
65+
// Add AWF installation or validation steps only if firewall is enabled
6666
if isFirewallEnabled(workflowData) {
67-
// Install AWF after Node.js setup but before Copilot CLI installation
6867
firewallConfig := getFirewallConfig(workflowData)
69-
var awfVersion string
70-
if firewallConfig != nil {
71-
awfVersion = firewallConfig.Version
72-
}
7368

74-
// Install AWF binary
75-
awfInstall := generateAWFInstallationStep(awfVersion)
76-
steps = append(steps, awfInstall)
69+
if firewallConfig == nil || firewallConfig.Path == "" {
70+
// Default: Download and install AWF from GitHub releases
71+
var awfVersion string
72+
if firewallConfig != nil {
73+
awfVersion = firewallConfig.Version
74+
}
75+
76+
awfInstall := generateAWFInstallationStep(awfVersion)
77+
steps = append(steps, awfInstall)
78+
} else {
79+
// Custom path: Validate the binary exists and is executable
80+
validationStep := generateAWFPathValidationStep(firewallConfig.Path)
81+
steps = append(steps, validationStep)
82+
}
7783
}
7884

7985
// Add Copilot CLI installation step after AWF
@@ -249,11 +255,14 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st
249255
awfArgs = append(awfArgs, firewallConfig.Args...)
250256
}
251257

258+
// Get AWF binary path (custom or default)
259+
awfBinary := getAWFBinaryPath(firewallConfig)
260+
252261
// Build the full AWF command with proper argument separation
253262
// AWF v0.2.0 uses -- to separate AWF args from the actual command
254263
// The command arguments should be passed as individual shell arguments, not as a single string
255264
command = fmt.Sprintf(`set -o pipefail
256-
sudo -E awf %s \
265+
sudo -E %s %s \
257266
-- %s \
258267
2>&1 | tee %s
259268
@@ -264,7 +273,7 @@ if [ -n "$COPILOT_LOGS_DIR" ] && [ -d "$COPILOT_LOGS_DIR" ]; then
264273
sudo mkdir -p %s
265274
sudo mv "$COPILOT_LOGS_DIR"/* %s || true
266275
sudo rmdir "$COPILOT_LOGS_DIR" || true
267-
fi`, shellJoinArgs(awfArgs), copilotCommand, shellEscapeArg(logFile), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder))
276+
fi`, shellEscapeArg(awfBinary), shellJoinArgs(awfArgs), copilotCommand, shellEscapeArg(logFile), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder), shellEscapeArg(logsFolder))
268277
} else {
269278
// Run copilot command without AWF wrapper
270279
command = fmt.Sprintf(`set -o pipefail
@@ -866,6 +875,50 @@ func generateAWFInstallationStep(version string) GitHubActionStep {
866875
return GitHubActionStep(stepLines)
867876
}
868877

878+
// resolveAWFPath handles path resolution for absolute and relative paths
879+
func resolveAWFPath(customPath string) string {
880+
if customPath == "" {
881+
return "/usr/local/bin/awf"
882+
}
883+
884+
if strings.HasPrefix(customPath, "/") {
885+
return customPath // Absolute path
886+
}
887+
888+
// Relative path - resolve against GITHUB_WORKSPACE
889+
return fmt.Sprintf("${GITHUB_WORKSPACE}/%s", customPath)
890+
}
891+
892+
// getAWFBinaryPath returns appropriate AWF binary path for execution
893+
func getAWFBinaryPath(firewallConfig *FirewallConfig) string {
894+
if firewallConfig != nil && firewallConfig.Path != "" {
895+
return resolveAWFPath(firewallConfig.Path)
896+
}
897+
return "awf" // Default (in PATH from installation step)
898+
}
899+
900+
// generateAWFPathValidationStep creates a validation step to verify custom AWF binary
901+
func generateAWFPathValidationStep(customPath string) GitHubActionStep {
902+
resolvedPath := resolveAWFPath(customPath)
903+
904+
stepLines := []string{
905+
" - name: Validate custom AWF binary",
906+
" run: |",
907+
fmt.Sprintf(" echo \"Validating custom AWF binary at: %s\"", resolvedPath),
908+
fmt.Sprintf(" if [ ! -f %s ]; then", shellEscapeArg(resolvedPath)),
909+
fmt.Sprintf(" echo \"Error: AWF binary not found at %s\"", resolvedPath),
910+
" exit 1",
911+
" fi",
912+
fmt.Sprintf(" if [ ! -x %s ]; then", shellEscapeArg(resolvedPath)),
913+
fmt.Sprintf(" echo \"Error: AWF binary at %s is not executable\"", resolvedPath),
914+
" exit 1",
915+
" fi",
916+
fmt.Sprintf(" %s --version", shellEscapeArg(resolvedPath)),
917+
}
918+
919+
return GitHubActionStep(stepLines)
920+
}
921+
869922
// generateSquidLogsCollectionStep creates a GitHub Actions step to collect Squid logs from AWF
870923
func generateSquidLogsCollectionStep(workflowName string) GitHubActionStep {
871924
sanitizedName := strings.ToLower(SanitizeWorkflowName(workflowName))

pkg/workflow/firewall.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type FirewallConfig struct {
1313
Args []string `yaml:"args,omitempty"` // Additional arguments to pass to AWF
1414
LogLevel string `yaml:"log_level,omitempty"` // AWF log level (default: "info")
1515
CleanupScript string `yaml:"cleanup_script,omitempty"` // Cleanup script path (default: "./scripts/ci/cleanup.sh")
16+
Path string `yaml:"path,omitempty"` // Custom AWF binary path (skips download when specified)
1617
}
1718

1819
// isFirewallEnabled checks if AWF firewall is enabled for the workflow

0 commit comments

Comments
 (0)